#!/usr/bin/env bash

export LANG="C"
export LC_ALL="C"

GEM_RE='([^0-9].*)-([0-9].*)'
PULL_BUNDLE_RE='Crowbar-Pull-ID: ([0-9a-f]{40})'
PULL_RELEASE_RE='Crowbar-Release: ([^ ]+)'
PULL_TITLE_RE='^(.*)\[([0-9]+)/([0-9]+)\]$'

readonly currdir="$PWD"
export PATH="$PATH:/sbin:/usr/sbin:/usr/local/sbin"
# Ubuntu hack to make sure gem executables are in our path.
[[ -d /var/lib/gems/1.9.1/bin ]] && export PATH=$PATH:/var/lib/gems/1.9.1/bin
declare -A DEV_BRANCHES DEV_REMOTE_SOURCES DEV_REMOTE_BRANCHES DEV_ORIGIN_TYPES
declare -A DEV_REMOTE_PRIORITY DEV_REMOTE_URLBASE __AVAILABLE_REMOTES
declare -a DEV_SORTED_REMOTES
declare -A DEV_COMMANDS DEV_SHORT_HELP DEV_LONG_HELP
# The generation of the dev tool this is.
readonly DEV_VERSION=2

# The key -> value mapping in DEV_BRANCHES defines child -> parent relations
# between branches.  A branch is considered a root branch if it has itself
# as a parent.
DEV_BRANCHES["master"]="master"
DEV_BRANCHES["openstack-os-build"]="master"
DEV_BRANCHES["hadoop-os-build"]="master"
DEV_BRANCHES["cloudera-os-build"]="master"
github_re='^(https?|ssh|git)://.*github\.com/([^/]+)'

# Associative array to handle skeleton of the state transition machine.
# This is roughly a reverse lookup table for use by ci_get_state().
# The state transition rules are implemented by ci_all_next_states()
declare -A CI_STATES
CI_STATES["new"]="failed needs-work"
CI_STATES["merge-testing"]="new"
CI_STATES["merge-tested"]="merge-testing"
# These are shortened versions of the real next states.
# The real next states are unit-(testing|tested)-release/build
CI_STATES["unit-testing"]="merge-tested"
CI_STATES["unit-tested"]="unit-testing"
# These are also shortened versions.  The real states are:
# (build|smoke)-(testing|tested)-<release>/<build>/<os>
CI_STATES["build-testing"]="merge-tested unit-tested"
CI_STATES["build-tested"]="build-testing"
CI_STATES["smoke-testing"]="build-tested"
CI_STATES["smoke-tested"]="smoke-testing"
# These are the actual states.
CI_STATES["code-reviewing"]="smoke-tested"
CI_STATES["needs-work"]="code-reviewing"
CI_STATES["code-reviewed"]="code-reviewing"
CI_STATES["mergeable"]="code-reviewed"
CI_STATES["merged"]="mergeable"
CI_STATES["failed"]="all"
CI_STATES["closed"]="all"


# DEV_REMOTE_BRANCHES defines what branches in the main Crowbar repository
# should be pulled and synced with what remotes.
# Barclamps do care about remote branches.
DEV_REMOTE_BRANCHES["origin"]="master openstack-os-build hadoop-os-build cloudera-os-build"
VERBOSE=true
# Source our config file if we have one
[[ -f $HOME/.build-crowbar.conf ]] && \
    . "$HOME/.build-crowbar.conf"

# Look for a local one.
[[ -f build-crowbar.conf ]] && \
    . "build-crowbar.conf"

# Location of the Crowbar checkout we are building from.
[[ $CROWBAR_DIR ]] || CROWBAR_DIR="${0%/*}"
[[ $CROWBAR_DIR = /* ]] || CROWBAR_DIR="$currdir/$CROWBAR_DIR"
. "$CROWBAR_DIR/build_lib.sh" || exit 1
[[ -f $CROWBAR_DIR/build_crowbar.sh && -d $CROWBAR_DIR/.git ]] || \
    die "$CROWBAR_DIR is not a git checkout of Crowbar!"
[[ $CI_TRACKING_REPO ]] || CI_TRACKING_REPO=ci-tracking
[[ $CROWBAR_TEST_DIR ]] || CROWBAR_TEST_DIR="/tmp/crowbar-dev-test"
[[ $LOCAL_PULL_TRACKING ]] || LOCAL_PULL_TRACKING="$CROWBAR_DIR/.ci-tracking"
[[ $OPEN_PULL_REQUESTS ]] || OPEN_PULL_REQUESTS="$LOCAL_PULL_TRACKING/local"
export CROWBAR_DIR CROWBAR_TEST_DIR LOCAL_PULL_TRACKING
export OPEN_PULL_REQUESTS

trap - 0 INT QUIT TERM
trap 'rm -rf "$CROWBAR_TMP"' 0 INT QUIT TERM
which gem &>/dev/null || \
    die "Rubygems not installed, and some of our helpers need a JSON gem."

gem list -i json &>/dev/null || \
    die "JSON gem not installed.  Please install it with gem install json."

[[ $(type -p curl 2>/dev/null) ]] || \
    die "No curl installed.  Please install curl."

set -o pipefail
shopt -s extglob
shopt -s nullglob

# A little wrapper for git to help people see what dev is doing.
git() {
    [[ $SHOW_GIT_OPERATIONS ]] && debug "$PWD: git $@"
    case $1 in
        rebase|merge|push|commit)
            [[ $(git config --get user.name) && $(git config --get user.email) ]] || \
                die "Git not setup." \
                "Please run" \
                "git config --global user.name \"your name\"" \
                "git config --global user.email \"you@your.domain\"";;
    esac
    command git "$@"
}

# Test to see if a remote is available.
# Filters based on $DEV_AVAILABLE_REMOTES
remote_available() {
    # $1 = remote to test.
    # Returns 0 if the remote is available, 1 otherwise.
    local r urlbase url_re='^(file|https?|ssh|git)://([^/]+)'
    local -A remote_hash
    if [[ ! $DEV_AVAILABLE_REMOTES ]]; then
        crowbar_remote_exists "$1"
        return $?
    fi
    if [[ ${__AVAILABLE_REMOTES[$1]} ]]; then
        [[ ${__AVAILABLE_REMOTES[$1]} = true ]]
        return $?
    fi
    # Handle "origin" specially as shorthand for $(origin_remote)
    for r in ${DEV_AVAILABLE_REMOTES//origin/$(origin_remote)}; do
        crowbar_remote_exists "$r" || \
            die "Unknown remote $r in DEV_AVAILABLE_REMOTES!"
        urlbase=${DEV_REMOTE_URLBASE[$r]}
        [[ $urlbase =~ $url_re ]] || continue
        remote_hash["${BASH_REMATCH[2]}"]=true
    done
    urlbase=${DEV_REMOTE_URLBASE[$1]}
    [[ $urlbase =~ $url_re ]] || return 1
    if [[ ${BASH_REMATCH[1]} = file || ${remote_hash["${BASH_REMATCH[2]}"]} ]]; then
        __AVAILABLE_REMOTES[$1]=true
        return 0
    else
        __AVAILABLE_REMOTES[$1]=false
        return 1
    fi
}

# The remote with the highest priority is considered to be the "origin" remote.
# This is a function and not a variable because not all the repos we work
# with will have the same origin remotes.
origin_remote() {
    local r
    for r in "${DEV_SORTED_REMOTES[@]}"; do
        git_remote_exists "$r" || continue
        echo "$r"
        return 0
    done
    return 1
}

# Check out the appropriate branches in each barclamp needed to switch to
# the new build.  This function takes care to minimize the amount of branch
# swizzling that occurs.
switch_barclamps_to_build() {
    # $1 = new build
    local bc new_head current_head
    build_exists "$1" || die "Build $1 does not exist"
    for bc in $(barclamps_in_build "$1"); do
        [[ -d $CROWBAR_DIR/barclamps/$bc/.git ]] || \
            die "Build $1 requires $bc, which is not available locally." \
            "dev clone-barclamps should fetch it, if not you may not have access to it."
    done
    for bc in "$CROWBAR_DIR/barclamps/"*; do
        bc=${bc##*/}
        new_branch=$(barclamp_branch_for_build "$1" "$bc")
        current_head=$(in_barclamp "$bc" get_current_head)
        if [[ $new_branch =~ [0-9a-f]{40} && $new_branch = $current_head ]]; then
            # We want HEAD to be on a raw commit, and it is on the one we want.
            continue
        elif [[ $current_head = $new_branch ]]; then
            # We want HEAD to be on a branch, and it is on the one we want.
            continue
        elif [[ $new_branch = empty-branch ]]; then
            # We want to be on the empty branch, and we are not. Switch to the
            # empty branch, creating it if we have to.
            debug "Switching $bc to the empty branch."
            in_barclamp "$bc" to_empty_branch
        else
            # We are not where we want to be.  Go there.
            debug "Switching $bc to $new_branch"
            in_barclamp "$bc" quiet_checkout "$new_branch" && continue
            die "$new_branch does not exist in $bc!" \
                "You should try to fix it with a ./dev fetch followed by ./dev sync." \
                "If that does not work, then someone forgot to push the branch when" \
                "$1 was created."
        fi
    done
    # Record that we are on the new build.
    in_repo git config 'crowbar.release' "${1%/*}"
    in_repo git config 'crowbar.build' "$1"
}

# Check out the appropriate release branch for all the barclamps in a given
# release or build.
switch_release() {
    # $1 = build or release to switch to
    local l br bc current_branch new_base rel repo
    local -A barclamps
    new_build="$(current_build)"
    if [[ $1 ]]; then
        if build_exists "$1"; then
            new_build="$1"
        elif release_exists "$1"; then
            new_build="$1/${new_build##*/}"
            if ! build_exists "$new_build"; then
                debug "Release $1 does not have build $new_build, switching to $1/master instead."
                new_build="$1/master"
            fi
        else
            die "$1 is not a release or a build I can switch to!"
        fi
    fi
    barclamps_are_clean || \
        die "Crowbar repo must be clean before trying to switch releases!"
    switch_barclamps_to_build "$new_build"
    __switch_release_helper "$new_build"
    debug "Switched to $new_build"
}

# This function is kept around for legacy workflow reasons.
checkout() {
    local br new_build rel
    rel="$(current_release)"
    new_build="$rel/$1"
    switch_barclamps_to_build "$rel/$1"
}

# Test to see of we are on the right metadata version.
dev_is_setup() {
    in_repo git_config_has crowbar.dev.version || [[ $1 = setup ]] || return 1
    local thisrev
    thisrev=$(get_repo_cfg crowbar.dev.version) && \
        (( thisrev  == DEV_VERSION )) || [[ $1 = setup ]] || {
        VERBOSE=true
        debug "Crowbar repository out of sync with dev tool revision."
        debug "Please run $0 setup to update it."
        exit 1
    }
}

# Given a branch, print the first remote that branch is "owned" by.
# This assumes update_tracking_branches is keeping things up to date.
remote_for_branch() {
    local -a remotes
    local remote
    if [[ ! $DEV_FROM_REMOTES ]]; then
        if git_config_has "branch.$1.remote"; then
            git config --get "branch.$1.remote"
            return $?
        else
            remotes=("${DEV_SORTED_REMOTES[@]}")
        fi
    fi
    [[ $DEV_FROM_REMOTES ]] && remotes=("${DEV_FROM_REMOTES[@]}")
    for remote in "${remotes[@]}"; do
        git show-ref --quiet --verify "refs/remotes/$remote/$1" || continue
        echo "$remote"
        return 0
    done
    return 1
}

# Test to see if a specific repository is clean.
# Ignores submodules and unchecked-in directories that are git repositories.
git_is_clean() {
    local line hpath ret=0 opt
    local stat_cmd="git status --porcelain" quiet=''
    local paths=()
    while [[ $1 ]]; do
        opt="$1"; shift
        case $opt in
            --barclamps) stat_cmd="git status --porcelain";;
            -q|--quiet) quiet=true;;
            --paths)
                while [[ $1 && $1 != '-'* ]]; do
                    paths+=("$1")
                    shift
                done;;
            *) die "Unknown option $opt passed to git_is_clean.";;
        esac
    done
    [[ $paths ]] && stat_cmd+=" -- ${paths[*]}"
    while read line; do
        case $line in
            # Untracked file.  Ignore it if it is also a git repo,
            # complain otherwise.
            '??'*) hpath=${line%% ->*}
                hpath=${hpath#* }
                [[ -d $PWD/$hpath.git || -f $PWD/$hpath.git || $hpath = barclamps/ ]] && continue
                ret=1; break;;
            '') continue;;
            *) ret=1; break;;
        esac
    done < <($stat_cmd)
    [[ $ret = 0 ]] && return
    [[ $quiet ]] || {
        echo "$PWD:"
        git status -- "${paths[@]}"
    }
    [[ $IGNORE_CLEAN ]] && return 0
    return 1
}

# Stupid wrapper around git push for debugging.
git_push() {
    if [[ $DEBUG = true || $DRY_RUN ]]; then
        echo "would have done git push $@"
        return
    fi
    git push "$@"
}

# Test to see if a barclamp is clean.
barclamp_is_clean() {
    local bc="$1"; shift
    in_barclamp "$bc" git_is_clean "$@"
}

# Test to see if all our barclamps are clean.
barclamps_are_clean() {
    local bc res=0
    for bc in "$CROWBAR_DIR/barclamps/"*; do
        is_barclamp "${bc##*/}" || continue
        in_barclamp "${bc##*/}" git_is_clean "$@" || res=1
    done
    return $res
}

# Test to see if all the Crowbar repositories are clean.
crowbar_is_clean() {
    local res=0
    barclamps_are_clean "$@" && in_repo git_is_clean "$@" && return 0
    echo "Your crowbar repositories are not clean."
    echo "Please review the git status output above, and add and commit/stash as needed."
    return 1
}

# Check to see if a remote exists.
test_remote() { git ls-remote "$1" "refs/heads/master" &> /dev/null; }

# Fork a barclamp on Github.
fork_barclamp() {
    # $1 = remote to fork from.  Must be a github remote.
    # $2 = barclamp to fork.
    # $3 = remote on Github to fork to.  Must be on Github. and defaults to personal.
    local from_remote="${DEV_REMOTE_URLBASE[$1]}"
    local to_remote="${DEV_REMOTE_URLBASE[${3:-personal}]}"
    if ! [[ $from_remote && $from_remote =~ $github_re ]]; then
        die "Source remote $1 is not Github remote!"
    elif ! [[ $to_remote && $to_remote =~ $github_re ]]; then
        [[ $3 ]] && die "Target remote $3 is not a Github remote!"
        [[ $to_remote ]] || die "Personal remote not configured, cannot fork $2 to it."
        die "Personal remote does not point at a Github remote, cannot fork $2 to it."
    elif [[ $to_remote != */$DEV_GITHUB_ID ]]; then
        die "Remote ${3:-personal} does not map to your Github account!"
    fi
    test_remote "$to_remote/barclamp-$2.git" && return 0 # already forked
    test_remote "$from_remote/barclamp-$2.git" || die "Barclamp $2 does not exist at remote $1"
    github_fork "${DEV_REMOTE_URLBASE[$1]##*/}" "barclamp-$2"
}

# Look for barclamps that a build references, but that we don't have locally
find_missing_barclamps() {
    # $1 = '', release, release/build
    local bc barclamps=()
    if build_exists "$1"; then
        barclamps=($(barclamps_in_build "$1"))
    elif release_exists "$1"; then
        barclamps=($(barclamps_in_release "$1"))
    else
        barclamps=($(all_barclamps))
    fi
    for bc in "${barclamps[@]}"; do
        bc="$CROWBAR_DIR/barclamps/$bc"
        [[ -d $bc/.git || -f $bc/.git ]] && continue
        echo "${bc##*/}"
    done
}

# Look for barclamps that exist locally, but that are not referenced by
# any local builds.
find_orphaned_barclamps() {
    local bc
    local -A barclamps
    for bc in "$CROWBAR_DIR/barclamps/"*; do
        [[ -d $bc/.git || -f $bc/.git ]] || continue
        barclamps["${bc##*/}"]=present
    done
    for bc in $(all_barclamps); do
        [[ ${barclamps[$bc]} ]] && continue
        echo "$bc"
    done
}

# Check to see if a barclamp exists at a given remote.
# Barclamps can either exist at:
#   barclamp-$1 for Github, Bitbucket and the like, or
#   barclamps/$1 for trees that have been layed out by dev.
# We test for both to make it possible to use dev setup for local clones.
probe_barclamp_remote() {
    # $1 = barclamp
    # $2 = urlpart
    local remote
    for remote in "barclamp-$1" "crowbar/barclamps/$1"; do
        test_remote "$2/$remote" || continue
        echo "$remote"
        return 0
    done
    return 1
}

# Perform an initial clone of a barclamp.
# Takes care to ensure that the origin remote is set appropriatly.
# This will also create a personal remote if needed.
clone_and_sync_barclamp() {
    # $1 = name of the barclamp
    local repo remote build head urlbase
    if ! [[ -d $CROWBAR_DIR/barclamps/$1/.git || \
        -f $CROWBAR_DIR/barclamps/$1/.git ]]; then
        for remote in "${DEV_SORTED_REMOTES[@]}"; do
            urlbase="${DEV_REMOTE_URLBASE[$remote]}"
            repo="$(probe_barclamp_remote "$1" "$urlbase")" && break
        done
        [[ $repo ]] || return 1
        in_repo git clone -l -o "$remote" "$urlbase/$repo" "barclamps/$1" || {
            rm -rf "$CROWBAR_DIR/barclamps/$1"
            die "Unable to clone barclamp $bc from $urlbase/$repo.git"
        }
    fi
    [[ -f $CROWBAR_DIR/barclamps/$1/.git && \
        ! -d $CROWBAR_DIR/barclamps/$1/.git ]] && (
        cd "$CROWBAR_DIR/barclamps/$1/"
        debug "De-submoduleizing barclamp ${1}"
        read gpath < ".git"
        if [[ $gpath = 'gitdir: '* ]]; then
            rm -f ".git"
            mv "${gpath#gitdir: }" ".git"
            (export GIT_WORK_TREE=.; git config --unset core.worktree)
            git reset --hard HEAD
            git clean -f -x -d
        else
            echo "Malformed .git file in $1. Skipping." >&2
        fi
    )

    sync_barclamp_remotes "$1"
    [[ $remote ]] || remote=$(in_barclamp "$1" origin_remote) || \
        die "Cannot find origin remote for barclamp $1!"
    urlbase="${DEV_REMOTE_URLBASE[$remote]}"
    if [[ $urlbase =~ $github_re ]] && remote_available personal && \
        ! in_barclamp "$1" git_remote_exists personal; then
        # test to see if we need to fork this barclamp at Github
        if ! github_repo_exists "barclamp-$1"; then
            debug "Creating a personal fork of barclamp-$1"
            fork_barclamp "$remote" "$1" || return 2
            sync_barclamp_remotes "$1"
        fi
    fi

    in_barclamp "$1" git_remote_exists origin && in_barclamp "$1" git remote rm origin
    in_barclamp "$1" update_tracking_branches
}

# Clone and synchronize remotes for barclamps on the command line, as needed.
clone_barclamps() {
    # $@ = barclamps to clone.  If none are passed, defaults to missing ones.
    local barclamps=() bc
    if [[ $1 = all ]]; then
        barclamps=($(all_barclamps))
    elif [[ $1 ]]; then
        barclamps=("$@")
    else
        barclamps=($(find_missing_barclamps))
    fi
    for bc in "${barclamps[@]}"; do
        clone_and_sync_barclamp "$bc" && continue
        case $? in
            1) debug "Unable to find barclamp $bc in any available remotes.";;
            2) debug "Unable to create a personal fork of barclamp $bc";;
        esac
    done
    # Create a fork of Crowbar if we just created a personal remote.
    if crowbar_remote_exists personal && ! github_repo_exists crowbar; then
        echo "Creating your fork of Crowbar on Github."
        github_fork "$(origin_remote)" crowbar || \
            die "Unable to create your fork of Crowbar."
    fi
}

# Update tracking branches for all remotes in a specific repo.
# Expects to be called within a specific repository
# It is structured to minimize forking, please be careful modifying it.
__update_tracking_branches() {
    # Create tracking branches for any branches from this remote
    # that do not already exist.
    local remote p br in_tracking_update=true
    local -A branches
    while read p br; do
        # We never care about HEAD branches at all.
        [[ ${br##*/} = HEAD ]] && continue
        if [[ $br = refs/heads/* ]]; then
            br="${br#refs/heads/}"
            if [[ $br = personal/* ]]; then
                # This should not be here.  Kill it.
                git branch -D "$br"
                # Nuke any config that might have come from a remote we don't care about.
                git_config_has "branch.$br.remote" || continue
                git config --remove-section "branch.$br"
                continue
            fi
            # Record that we have seen this branch, but don't know what its remote should be.
            branches["$br"]="no remote"
            continue
        elif [[ $br = refs/remotes/* ]]; then
            br="${br#refs/remotes/}"
            # Grab our remote, and go on to the next ref if we don't care about it.
            remote="${br%%/*}"
            [[ ${remotes[$remote]} ]] || continue
            br="${br#${remote}/}"
            # Skip personal or pull-request branches.
            [[ $br = personal/* || $br = pull-req* ]] && continue
            # If we have never seen this branch before, or
            # we don't have a remote for it, or
            # the remote we have for it is lower priority than our remote, then
            # give this branch us as a remote instead.
            if [[ ! ${branches[$br]} || \
                ${branches[$br]} = "no remote" ]] || \
                (( ${remotes[${branches[$br]}]} > ${remotes[$remote]} )); then
                branches[$br]="$remote"
            fi
        fi
    done < <(LC_ALL=C git show-ref |sort -k2) # Ensure that heads come first!
    # Now, we have our list of branches.  Operate on it.
    for br in "${!branches[@]}"; do
        remote="${branches[$br]}"
        if ! branch_exists "$br"; then
            # We need to create a local ref for this branch.
            git branch "$br" "$remote/$br"
            git config "branch.$br.remote" "$remote"
            git config "branch.$br.merge" "refs/heads/$br"
        elif [[ $remote = "no remote" ]]; then
            continue
        else
            git config --remove-section "branch.$br"
            git config "branch.$br.remote" "$remote"
            git config "branch.$br.merge" "refs/heads/$br"
        fi
    done
}

update_tracking_branches() {
    local p=0 remote
    local -A remotes
    for remote in "${DEV_SORTED_REMOTES[@]}"; do
        remotes[$remote]="$p"
        p=$((p + 1))
    done
    __update_tracking_branches
}

update_cache_tracking_branches() {
    local -A remotes
    remotes["origin"]=0
    in_cache __update_tracking_branches
}

# Update tracking references for all branches in all the
# repositories that dev is managing.
update_all_tracking_branches() {
    local bc
    debug "Updating tracking branch references in Crowbar"
    in_repo update_tracking_branches
    for bc in "$CROWBAR_DIR/barclamps/"*; do
        [[ -d $bc/.git || -f $bc/.git ]] || continue
        debug "Updating tracking branch references in barclamp ${bc##*/}"
        (cd "$bc"; update_tracking_branches)
    done
    if [[ -d $LOCAL_PULL_TRACKING/.git ]]; then
        debug "Updating tracking branch references in the CI tracking repo."
        (cd "$LOCAL_PULL_TRACKING"; update_tracking_branches)
    fi
}

# Helper for sourcing pull request data as variables.
source_prq_vars() (
    # $1 = path to pull request metadata
    # $2 = whether to scope the variables as local
    local f  val
    cd "$1" || return 1
    [[ -f source_repo && -f created_at && -f title ]] || return 1
    for f in *; do
        [[ -f $f ]] || continue
        read val < "$f"
        local s=''
        [[ $3 && $3 = "unset" ]] && s+="unset prq_$f; "
        [[ $2 && $2 = "local" ]] && s+="local "
        s+="prq_$f=\"$val\""
        echo "$s"
    done
)

# Check remote references at the personal remote to see if there
# are any merged pull requests.  If there are, delete them.
scrub_merged_pull_requests() {
    remote_available personal && remote_is_github personal || return 0
    # $@ = branches to test for mergedness
    local br ref pull_req remote
    local -A to_remove pull_reqs heads
    while read ref br; do
        case $br in
            refs/heads/*)
                ref=${br#refs/heads/}
                heads[${ref//\//-}]+="$br ";;
            refs/remotes/personal/pull-req-*)
                ref=${br#refs/remotes/personal/pull-req-}
                ref=${ref#heads-}
                ref=${ref%-*}
                ref=${ref%-0}
                pull_reqs["$br"]="$ref";;
        esac
    done < <(git show-ref)
    [[ ${pull_reqs[*]} ]] || return 0
    for pull_req in "${!pull_reqs[@]}"; do
        ref="${pull_reqs[$pull_req]}"
        [[ ${heads[$ref]} ]] || continue
        for br in ${heads["$ref"]}; do
            remote=$(remote_for_branch "$br")
            branches_synced . "$remote/$br" "$pull_req" || \
                [[ $1 = '--all' ]] || continue
            to_remove["${pull_req#refs/remotes/personal/}"]="true"
            continue 2
        done
    done
    [[ ${!to_remove[*]} ]] || return
    git_push --delete personal "${!to_remove[@]}"
    git remote prune personal
}

# Helper function only for use by fetch_pull_request_metadata.
# If we have applicable metadata from the CI repo, we will
# pull it in as well.
save_pull_request_metadata() (
    local fetchcmd
    [[ $base_branch && \
        $pull_req_branch && \
        $pull_req_repo && \
        $pull_req_target_repo && \
        $pull_req_sha && \
        $pull_req_state && \
        $repo && \
        $title && \
        $github_id && \
        $created_at  && \
        $github_target_user ]] || return
    if [[ $pull_req_id ]]; then
        prq="bundles/$pull_req_id/$repo"
        if [[ -d $LOCAL_CI_TRACKING/${prq%/*} && \
            ! -d $OPEN_PULL_REQUESTS/${prq%/*} ]]; then
            mkdir -p "$OPEN_PULL_REQUESTS/${prq%/*}"
            cp -a "$LOCAL_CI_TRACKING/${prq%/*}/." \
                "$OPEN_PULL_REQUESTS/${prq%/*}/."
        fi
    else
        prq="singletons/$github_target_user/$repo/$github_id"
        [[ $order ]] || order='1:1'
        if [[ -d $LOCAL_CI_TRACKING/${prq} && \
            ! -d $OPEN_PULL_REQUESTS/${prq} ]]; then
            mkdir -p "$OPEN_PULL_REQUESTS/${prq}"
            cp -a "$LOCAL_CI_TRACKING/${prq}/." \
                "$OPEN_PULL_REQUESTS/${prq}/."
        fi
    fi
    local dest="$OPEN_PULL_REQUESTS/$prq"
    mkdir -p "$dest"
    case $repo in
        crowbar) fetchcmd="in_repo git fetch";;
        barclamp-*) fetchcmd="in_barclamp "${repo#barclamp-}" git fetch";;
        *) die "Unknown repo $repo!";;
    esac
    local local_branch="pull-req/$github_user/$github_id"
    $fetchcmd "$pull_req_repo" "+$pull_req_branch:$local_branch" &>/dev/null || return
    cd "$dest"
    echo "$order" > order
    echo "$pull_req_repo" > source_repo
    echo "$repo" > local_repo
    echo "$pull_req_target_repo" > target_repo
    echo "$pull_req_branch" > source_branch
    echo "$pull_req_sha" > source_sha
    echo "$pull_req_target_sha" > target_sha
    echo "$base_branch" > target_branch
    echo "$github_id" > number
    echo "$created_at" > created_at
    echo "$title" > title
    echo "$pull_req_state" > state
    echo "$github_url" > github_url
    echo "$local_branch" > local_branch
    echo "$github_user" > source_account
    echo "$github_target_user" > target_account
    [[ $updated_at ]] && echo "$updated_at" > updated_at
    if [[ $rel ]]; then
        echo "$rel" > release
    else
        echo "$(release_for_branch "$base_branch")" >release
    fi
)

# Clear out local pull request metadata, including local pull request branches.
clear_pull_request_metadata() (
    for bc in "$CROWBAR_DIR/barclamps/"* "$CROWBAR_DIR"; do
        [[ -d $bc/.git || -f $bc/.git ]] || continue
        cd "$bc"
        while read sha ref; do
            [[ $ref = refs/heads/pull-req/* ]] || continue
            git branch -D ${ref#refs/heads/} &>/dev/null
        done < <(git show-ref --heads)
    done
    [[ -d $OPEN_PULL_REQUESTS ]] && rm -rf "$OPEN_PULL_REQUESTS"
)

# Fetch pull request metadata from Github.
# When this function is done, metadata for all pull requests generated by
# dev for this repo will have the right metadata fragments in the tracking directory
# for the repository in question.
fetch_pull_request_metadata() {
    # $1 = remote name
    # $2 = repository name.

    # Not a github remote?  Goodbye.
    [[ ${DEV_REMOTE_URLBASE[$1]} =~ $github_re ]] || return 0
    local acct="${BASH_REMATCH[2]}"
    # Get our raw data.  This is what we in the business call fugly.
    local -A pulls
    . <(parse_yml_or_json - pulls < <(
            curl_and_res "https://api.github.com/repos/$acct/$2/pulls"))
    local pull_req_id pull_req_sha order pull_req_repo pull_req_branch created_at
    local base_branch key ord repo title rel github_id updated_at github_url github_user
    local github_target_user pull_req_target_repo pull_req_target_sha pull_req_state
    for key in $(printf '%s\n' "${!pulls[@]}" |sort); do
        if [[ $ord && $ord != ${key%%.*} ]]; then
            # The first part of the key has changed, so we should have a complete set of data.
            save_pull_request_metadata
            unset pull_req_id pull_req_sha order pull_req_repo pull_req_branch created_at
            unset base_branch key ord repo title rel github_id updated_at github_url github_user
            unset github_target_user pull_req_target_repo pull_req_target_sha pull_req_state

            local pull_req_id pull_req_sha order pull_req_repo pull_req_branch created_at
            local base_branch key ord repo title rel github_id updated_at github_url github_user
            local github_target_user pull_req_target_repo pull_req_target_sha pull_req_state

        fi
        ord=${key%%.*}
        # Strip off the inital array component of the hash key.
        # We will not need it anyways.
        case ${key#*.} in
            body)
                # If the body text does not have a pull request bundle ID, skip it.
                [[ ${pulls[$key]} =~ $PULL_BUNDLE_RE ]] && pull_req_id="${BASH_REMATCH[1]}"
                [[ ${pulls[$key]} =~ $PULL_RELEASE_RE ]] && rel="${BASH_REMATCH[1]}";;
            # The repo and branch that contains changes to be tested.
            head.ref) pull_req_branch="${pulls[$key]}";;
            head.repo.owner.login) github_user="${pulls[$key]}";;
            base.repo.owner.login) github_target_user="${pulls[$key]}";;
            head.sha) pull_req_sha="${pulls[$key]}";;
            head.repo.clone_url) pull_req_repo="${pulls[$key]}";;
            base.repo.clone_url) pull_req_target_repo="${pulls[$key]}";;
            base.sha) pull_req_target_sha="${pulls[$key]}";;
            title)
                if [[ ${pulls[$key]} =~ $PULL_TITLE_RE ]]; then
                    order="${BASH_REMATCH[2]}:${BASH_REMATCH[3]}"
                    title="${BASH_REMATCH[1]}"
                else
                    title="${pulls[$key]}"
                fi;;
            base.ref) base_branch="${pulls[$key]}";;
            base.repo.name) repo="${pulls[$key]}";;
            number) github_id="${pulls[$key]}";;
            state)  pull_req_state="${pulls[$key]}";;
            created_at) created_at="${pulls[$key]}";;
            updated_at) updated_at="${pulls[$key]}";;
            html_url) github_url="${pulls[$key]}";;
        esac
    done
    save_pull_request_metadata
}

# Fetch pull request metadata for all repostories at a given remote.
fetch_pull_requests_for_remote() {
    # $@ = remote to check for updates from
    local -a remotes
    local remote
    [[ $1 ]] && remotes=("$@") || remotes=($(origin_remote))
    fetch_ci_tracking
    clear_pull_request_metadata
    for remote in "${remotes[@]}"; do
        if ! (crowbar_remote_exists "$remote" && remote_available "$remote" && \
            [[ ${DEV_REMOTE_URLBASE[$remote]} =~ $github_re ]]); then
            debug "Cannot fetch pull requests from $remote, skipping."
            continue
        fi
        debug "Fetching pull requests from $remote:"
        for bc in "$CROWBAR_DIR/barclamps/"*; do
            [[ -d $bc/.git || -f $bc/.git ]] || continue
            in_barclamp "${bc##*/}" git_remote_exists "$remote" || continue
            debug "Fetching open pull requests for barclamp ${bc##*/}"
            fetch_pull_request_metadata "$remote" "barclamp-${bc##*/}"
        done
        debug "Fetching open pull requests for Crowbar"
        fetch_pull_request_metadata "$remote" "crowbar"
        echo
    done
    sort_pull_requests
    show_open_pull_requests
}

# Check to see of a pull request bundle is sane.
# Sanity consists of having all the pull requests we need and
# ensuring that the branch -> release mapping is consistent.
pull_request_bundle_sane() {
    # $1 = bundle ID.
    # Returns true if all components of the bundle are present, and if
    # all the target branches in the barclamps match the release we are
    # pulling against.
    local -A orders releases branches
    local -a order
    local total_count=0 indicated_count=0 d f r repo o c release branch
    local branch_for_release build bc source_account su tu
    for d in "$OPEN_PULL_REQUESTS/bundles/$1/"*; do
        [[ -d "$d" ]] || continue
        repo="${d##*/}"
        for f in order source_repo source_branch source_sha source_account \
            target_branch number created_at title release; do
            [[ -f $d/$f ]] && continue
            debug "Missing metadata info $d/$f"
            return 1
        done
        read o < "$d/order"
        read r < "$d/release"
        read su < "$d/source_account"
        read branch < "$d/target_branch"
        if ((indicated_count == 0)); then
            indicated_count="${o##*:}"
        elif ((indicated_count != "${o##*:}")); then
            debug "Indicated count changed for pull request bundle $1"
            return 1
        fi
        if [[ ! $release ]]; then
            release=$r
            if ! release_exists "$release"; then
                debug "Release $release does not exist locally."
                return 1
            fi
        elif [[ $r != $release ]]; then
            debug "Release changed in pull request bundle $1"
            return 1
        fi

        if [[ ! $source_account ]]; then
            source_account=$su
        elif
            [[ $su != $source_account ]]; then
            debug "Source Github account for pull request changed"
            return 1
        fi

        if [[ $d = *barclamp-* ]]; then
            bc="${d##barclamp-}"
            for build in $(builds_for_barclamp_in_release "$bc" "$release"); do
                [[ $branch = $(barclamp_branch_for_build "$build" "$bc") ]] && continue
                debug "Branch $branch for barclamp $bc in bundle $1 does not match metadata."
                return 1
            done
        fi
        (   cd "$d"
            for f in title release source_account; do
                [[ -f ../$f ]] || cp "$f" ..
            done )
        orders[$repo]=$o
        releases[$repo]=$release
        branches[$repo]=$branch
        total_count=$(($total_count + 1))
    done
    if ((indicated_count != total_count)); then
        debug "Not all barclamps in pull request bundle $1 at Github yet."
    fi
}

# Show a count of all open pull request bundles and singleton pull requests.
show_open_pull_requests() {
   local -a open_bundles open_singletons
   local d r
   for d in "$OPEN_PULL_REQUESTS/bundles/"*; do
       [[ -d $d ]] || continue
       pull_request_bundle_sane "${d##*/}" || continue
       open_bundles+=("$d")
   done
   for d in "$OPEN_PULL_REQUESTS/singletons/"*/*/*; do
       [[ -d $d ]] || continue
       open_singletons+=("$d")
   done
   if (( ${#open_bundles[@]} == 0 && ${#open_singletons[@]} == 0)); then
       r+="No open pull requests"
   elif (( ${#open_bundles[@]} == 0 )); then
       r+="${#open_singletons[@]} open singleton pull requests"
   elif (( ${#open_singletons[@]} == 0)); then
       r+="${#open_bundles[@]} open pull request bundles"
   else
       r+="${#open_bundles[@]} open pull request bundles and ${#open_singletons[@]} open singleton pull requests"
   fi
   debug "$r"
}

# Helper to determine when a pull request bundle was last touched.
pull_request_last_touched() (
    # $1 = bundle
    # Returns the most recent date that any of the bundle components were touched.
    local mtime
    if [[ $1 = bundles/* ]]; then
        cd "$OPEN_PULL_REQUESTS/$1" || die "Bundle $1 does not exist!"
        for d in *; do
            [[ -d $d ]] || continue
            [[ $ltime ]] || read ltime <"$d/created_at"
            [[ $ltime > $mtime ]] && mtime=$ltime
        done
        echo $mtime
    elif [[ $1 = singletons/* ]]; then
        cat "$OPEN_PULL_REQUESTS/$1/created_at"
    else
        die "No idea how to find last touched time for $1"
    fi
)

# Helper for finding out all the pull request metadata we have.
pull_request_directories() (
    [[ -d $OPEN_PULL_REQUESTS ]] || return 0
    cd "$OPEN_PULL_REQUESTS"
    for d in bundles/* singletons/*/*/*; do
        if [[ $d = bundles/* ]]; then
            pull_request_bundle_sane "${d##*/}" || continue
        fi
        [[ -f $d/title ]] || continue
        echo "$(pull_request_last_touched "$d")|$d"
    done |sort |cut -d \| -f 2
)

# Sort pull requests according to their last modification time.
sort_pull_requests() {
    local count=1 p
    rm "$OPEN_PULL_REQUESTS/pull_request_index" &>/dev/null
    for p in $(pull_request_directories); do
        echo "$count $p" >>"$OPEN_PULL_REQUESTS/pull_request_index"
        count=$((count + 1))
    done
}

# translate a pull request number (as displayed by dev pull-requests list)
# into an internal ID.
pull_request_number_to_id() {
    local idx id
    if [[ $1 && $1 =~ ^[0-9]+$ ]]; then
        read idx id < <(grep "^$1 " "$OPEN_PULL_REQUESTS/pull_request_index")
        [[ $1 = $idx ]] || die "$1 is not a valid pull request." \
            "dev pull-requests list shows the open ones we know about."
    elif grep -q " $1\$" "$OPEN_PULL_REQUESTS/pull_request_index"; then
        id="$1"
    elif [[ -d $LOCAL_PULL_TRACKING/$1/ci_state ]]; then
        id="$1"
    else
        die "$1 is not a valid pull request ID"
    fi
    echo $id
}

# Figure our which builds need to be built to test a given pull request
builds_for_one_pull_request() {
    # $1 =path to pull request
    local -a builds
    case $prq_local_repo in
        barclamp-*)
            builds=($(builds_for_barclamp_in_release "${prq_local_repo#barclamp-}" "$prq_release" ));;
        crowbar)
            builds=($(builds_in_release "$prq_release"));;
        *) die "Unknown repo $prq_local_repo in builds_for_one_pull_request!";;
    esac
    echo "${builds[*]}"
}

# Return a list of builds that a pull request should trigger.
# This is based on barclamp membership.
# Returns a list of builds in the order in which they should be built.
builds_for_pull_request() {
    local idx pr_id b release build os repo prq builds_for_release=() all_oses=()
    local -A oses valid_builds PULL_REQUEST_BARCLAMPS barclamp_oses build_oses _t _r
    idx=$1
    pr_id=$(pull_request_number_to_id "$idx") || exit 1
    prq="$OPEN_PULL_REQUESTS/$pr_id"
    read release <"$prq/release"
    case $pr_id in
        singletons/*)
            . <(source_prq_vars "$prq" "local")
            if [[ $prq_local_repo = barclamp-* ]]; then
                PULL_REQUEST_BARCLAMPS[${prq_local_repo#barclamp-}]="$prq_local_branch"
            fi
            for b in $(builds_for_one_pull_request "$prq"); do
                valid_builds[$b]="build"
            done;;
        bundles/*)
            for repo in "$prq"/*; do
                [[ -d $repo ]] || continue
                . <(source_prq_vars "$repo" "local")
                [[ $prq_local_repo = barclamp-* ]] && \
                    PULL_REQUEST_BARCLAMPS[${prq_local_repo#barclamp-}]="$prq_local_branch"
                for b in $(builds_for_one_pull_request "$repo"); do
                    valid_builds[$b]="build"
                done
            done;;
    esac
    # Now, we have all the possibly-applicable builds.
    # Get all the possible OSes for all possible barclamps in those builds, and use
    # it to derive all the OSes applicable to a build.
    all_oses=($(all_supported_oses))
    for build in "${!valid_builds[@]}"; do
        build_oses[$build]="${all_oses[*]}"
        for b in $(barclamps_in_build "$release/$build"); do
            if [[ ! -d $CROWBAR_DIR/barclamps/$b/.git ]]; then
                echo "You are missing the $b barclamp needed for $release/$build. Skipping." >&2
                valid_builds[$build]=skip
                continue 2
            fi
            if ! [[ ${barclamp_oses[$b]} ]]; then
                local br
                if [[ ${PULL_REQUEST_BARCLAMPS[$b]} ]]; then
                    br="${PULL_REQUEST_BARCLAMPS[$b]}"
                else
                    br="$(barclamp_branch_for_build "$release/$build" "$b")"
                fi
                if ! in_barclamp "$b" branch_exists "$br"; then
                    echo "Your $b barclamp in $release/$build does not have branch $br.Skipping." >&2
                    valid_builds[$build]=skip
                    continue 2
                fi
                p=$(extract_barclamp_metadata "$b" "$br")
                barclamp_oses[$b]=$(read_barclamp_metadata "$p" "barclamp" "os_support")
                [[ ${barclamp_oses[$b]} ]] || barclamp_oses[$b]=none
            fi
            if [[ ${barclamp_oses[$b]} && ${barclamp_oses[$b]} != none ]]; then
                unset _r
                local -A _r
                for os in ${barclamp_oses[$b]}; do _r[$os]=$b; done
                for os in ${build_oses[$build]}; do
                    [[ ${_r[$os]} ]] && continue
                    unset _r[$os]
                done
                build_oses[$build]="${!_r[*]}"
            fi
        done
    done
    # Now, filter the per-OS lists down to leaf nodes.
    builds_for_release=($(builds_in_release "$release"))
    # Filter out the builds that will be masked.
    local build parent_build
    for build in "${builds_for_release[@]}"; do
        [[ ${valid_builds[$build]} ]] || continue
        # Find the parent for this build.
        parent_build="$(parent_build "$build")"
        # Special case for the master build.
        [[ $parent_build = $build ]] && continue
        # If we wanted to build the parent for the current build, don't.
        # It will be implicitly tested when we build this build.
        if [[ $parent_build && ${valid_builds[$parent_build]} ]]; then
             valid_builds[$parent_build]=skip
        fi
    done
    # Display the ones that did not get masked
    for build in "${builds_for_release[@]}"; do
        [[ ${valid_builds[$build]} && ${valid_builds[$build]} = build ]] || continue
        echo "$release/$build: ${build_oses[$build]}"
    done
}

# Show information for a pull request in human-readable format.
show_pull_request() {
    local idx id prq repo release target_account state
    local -a builds ci_states
    idx=$1
    id=$(pull_request_number_to_id $idx) || exit 1
    prq="$OPEN_PULL_REQUESTS/$id"
    [[ -f $prq/title ]] || die "Something Wicked happened trying to show $idx." \
        "Please re-run dev pull-request fetch"
    if [[ $id = singletons/* ]]; then
        . <(source_prq_vars "$prq" "local")
        if [[ $prq_local_repo = barclamp-* ]]; then
            builds=($(builds_for_barclamp_in_release "${prq_local_repo#barclamp-}" "$prq_release"))
        else
            builds=($(builds_in_release "$prq_release"))
        fi
        echo "Title:               $prq_title"
        echo "Unique ID            $id"
        echo "Target Account:      $prq_target_account"
        echo "Source Account:      $prq_source_account"
        echo "Release:             $prq_release"
        echo "Last Updated:        $(pull_request_last_touched "$id")"
        echo "Type:                singleton"
        echo "Repo:                $prq_local_repo"
        echo "Repo with Changes:   $prq_source_repo"
        echo "Branch with Changes: $prq_source_branch"
        echo "Local Branch:        $prq_local_branch"
        echo "Target Branch:       $prq_target_branch"
        echo "Pull Request URL:    $prq_github_url"
        echo "Builds:              ${builds[*]}"
        if [[ -d $LOCAL_PULL_TRACKING/$id ]]; then
            echo "Current CI States:"
            while read state; do
                echo "    $state"
            done < <(ci_get_current_states "$id")
        fi
    elif [[ $id = bundles/* ]]; then
        echo "Title:          $(cat "$prq/title")"
        echo "Release:        $(cat "$prq/release")"
        echo "Last Updated:   $(pull_request_last_touched "$id")"
        echo "Type:           bundle"
        echo "Bundle ID:      ${id##*/}"
        if [[ -d $LOCAL_PULL_TRACKING/$id ]]; then
            echo "Current CI States:"
            while read state; do
                echo "    $state"
            done < <(ci_get_current_states "$id")
        fi
        local repo
        for repo in "$prq/"*; do
            [[ -d $repo ]] || continue
            . <(source_prq_vars "$repo" "local")
            if [[ $repo = */barclamp-* ]]; then
                builds=($(builds_for_barclamp_in_release "${repo#*/barclamp-}" "$prq_release"))
            else
                builds=($(builds_in_release "$prq_release"))
            fi
            echo "Repo: ${repo##*/}"
            echo "    Repo with Changes:   $prq_source_repo"
            echo "    Target Account:      $prq_target_account"
            echo "    Source Account:      $prq_source_account"
            echo "    Branch with Changes: $prq_source_branch"
            echo "    Local Branch:        $prq_local_branch"
            echo "    Target Branch:       $prq_target_branch"
            echo "    Pull Request URL:    $prq_github_url"
            echo "    Builds:              ${builds[*]}"
        done
    else
        die "Unknown pull request type $id!"
    fi
}

# List pull requests.  Uses the quasi-stable numbering provided by sorting.
list_pull_requests() {
    local idx id
    while read idx id; do
        [[ -d $OPEN_PULL_REQUESTS/$id ]] || continue
        cf="$OPEN_PULL_REQUESTS/$id/title"
        cr="$OPEN_PULL_REQUESTS/$id/release"
        if [[ -f $OPEN_PULL_REQUESTS/$id/number ]]; then
            cn=" $(cat $OPEN_PULL_REQUESTS/$id/number)"
        else
            cn=""
        fi
        ct="${id%%/*}"
        lrepo=""
        lurl=""
        prq="$OPEN_PULL_REQUESTS/$id"
        case $id in
            singletons/*)
               . <(source_prq_vars "$prq" "local")
               lrepo="$prq_local_repo"
               lurl=", $prq_github_url";;
            bundles/*)
                   lrepo="Multiple";;
        esac
        echo "$idx: ($(cat "$cr")$cn) $(cat "$cf")$lurl"
    done <"$OPEN_PULL_REQUESTS/pull_request_index"
}

pull_request_checkout_one() (
    . <(source_prq_vars "$1" "local")
    case $prq_local_repo in
        barclamp-*) cd "$CROWBAR_DIR/barclamps/${prq_local_repo#barclamp-}";;
        crowbar) cd "$CROWBAR_DIR";;
    esac
    debug "$prq_local_repo: Checking out $prq_local_branch"
    quiet_checkout "$prq_local_branch" || \
        die "Could not checkout $prq_local_branch!"
)

pull_request_switch_one() (
    . <(source_prq_vars "$1" "local")
    case $prq_local_repo in
        barclamp-*) cd "$CROWBAR_DIR/barclamps/${prq_local_repo#barclamp-}";;
        crowbar) cd "$CROWBAR_DIR";;
    esac
    pull_request_checkout_one "$1" || exit 1
    git checkout -q "$(git rev-parse HEAD)"
    git merge "$(git rev-parse "$prq_target_branch")" && return 0
    git merge --abort
    die "Could not merge $prq_target_branch into $prq_local_branch!"
)

# Switch to a specific build in a pull request.
# The release is inferred by the preferred release of the build request.
pull_request_switch() {
    # $1 = the local number of the pull request.
    # $2 = the build within the pull request. If only one
    #      build is applicable for a pull request, it defaults to that one.
    #      If more than one is applicable, you must set this arg.
    crowbar_is_clean || die "Trees must be clean before switching to a pull request"
    local id idx prq release bc barclamps build builds repo_dirs repo_dir repo res
    idx=$1
    id=$(pull_request_number_to_id "$idx") ||  \
        die "$id is not a valid pull request. dev pull-requests list will help."
    while read build; do
        builds+=("${build%%:*}")
    done < <(builds_for_pull_request "$idx")
    [[ $builds ]] || \
        die "Pull request $idx has no associated builds!"
    if (( ${#builds[@]} == 1 )) && [[ ! $2 || $2 = $builds ]]; then
        build=${builds}
    elif [[ $2 ]] && ! is_in "$2" "${builds[*]}"; then
        die "Build $2 is not applicable to pull request $idx." \
            "Run ./dev pull-requests builds $idx to see what is."
    elif [[ ! $2 ]]; then
        die "You must pass a build to pull-requests switch, because there is no obvious default." \
            "Run ./dev pull-requests builds $idx to see what is available."
    else
        build=$2
    fi
    prq="$OPEN_PULL_REQUESTS/$id"
    switch_release "$build" || \
        die "Could not switch to release $build"
    res=0
    case $id in
        singletons/*)
            pull_request_switch_one "$prq"
            res=$?;;
        bundles/*)
            for repo in "$prq"/*; do
                [[ -d $repo ]] || continue
                if ((res == 0)); then
                    pull_request_switch_one "$repo" && continue
                    debug "Switch failed, skipping the rest."
                    res=1
                else
                    debug "Skipping $(cat "$repo/github_url")"
                fi
            done;;
    esac
    if ((res == 0)); then
        debug "You are now on build $build in pull request $idx."
        debug "Remember to use the --no-switch flag to ./dev build."
        debug "./dev switch will throw away the current tree state when you are finished."
    else
        switch_release
        die "Pull request $idx does not merge cleanly.  It should be updated or closed."
    fi
}

pull_request_multiswitch_one() (
    local merge_res
    . <(source_prq_vars "$1" "local")
    case $prq_local_repo in
        barclamp-*) cd "$CROWBAR_DIR/barclamps/${prq_local_repo#barclamp-}";;
        crowbar) cd "$CROWBAR_DIR";;
    esac
    debug "Merging $prq_github_url into $prq_local_repo"
    [[ -f README.empty-branch ]] && exit 0
    git checkout -q "$(git rev-parse HEAD)" &>/dev/null
    merge_res=$(git merge "$(git rev-parse "$prq_local_branch")") && return 0
    git merge --abort
    echo "$merge_res"
    return 1
)

# Switch to a loal tree consisting of the contents of a bunch of pull requests
# merged in together.  This is less intelligent than pull_requests_switch, but
# should allow for fater testing of tightly related pull requests.
pull_request_multiswitch() {
    local base_build=$1 prq_id id res=0 prq
    shift
    crowbar_is_clean || die "Trees must be clean before starting a multiswitch"
    switch_release "$base_build" || die "Cannot switch to $base_build"
    for prq_id in "$@"; do
        id=$(pull_request_number_to_id "$prq_id") ||  \
            die "$id is not a valid pull request. dev pull-requests list will help."
        [[ $id ]] || die "Cannot translate $prq_id into a pull request handle!"
        prq="$OPEN_PULL_REQUESTS/$id"
        case $id in
            singletons/*)
                pull_request_multiswitch_one "$prq" || res=1;;
            bundles/*)
                for repo in "$prq"/*; do
                    [[ -d $repo ]] || continue
                    ((res == 0)) && \
                        pull_request_multiswitch_one "$repo" || res=1
                done;;
            *) die "Cannot happen in pull_requests_multiswitch: prq_id";;
        esac
        ((res != 0)) && die "Failed to merge $prq_id"
    done
    debug "All pull requests merged locally."
    debug "Remember to use the --no-switch flag to ./dev build."
    debug "./dev switch will throw away the current tree state."
}

pull_request_merge_one () {
    # $1 = path to saved metadata
    . <(source_prq_vars "$1" local)
    # Skip if we have already been merged.
    if curl_and_res -f -X GET \
        "https://api.github.com/repos/$prq_target_account/$prq_local_repo/pulls/$prq_number/merge" &>/dev/null; then
        return 0
    fi
    json="{\"commit_message\": \"$prq_github_url\n\nMerged by devtool for $DEV_GITHUB_ID\"}"
    if curl_and_res -f -X PUT \
        -d "$json" \
        "https://api.github.com/repos/$prq_target_account/$prq_local_repo/pulls/$prq_number/merge" &>/dev/null; then
        debug "$prq_github_url merged."
        return 0
    else
        debug "Merge of $prq_github_url failed."
        return 1
    fi
}

# Check out the branches specific to a pull request.
# Unlike pull-requests merge, this leaves you on actual branches.
pull_request_checkout() {
    # $1 = index
    local id res=0 repo
    crowbar_is_clean || die "Trees must be clean before switching to a pull request!"
    id=$(pull_request_number_to_id "$1") || \
        die "$1 is not a valid pull request!"
    case $id in
        singletons/*)
            pull_request_checkout_one "$OPEN_PULL_REQUESTS/$id"
            res=$?;;
        bundles/*)
            for repo in "$OPEN_PULL_REQUESTS/$id/"*; do
                [[ -d $repo ]] || continue
                if ((res == 0)); then
                    pull_request_checkout_one "$repo" && continue
                    debug "Skipping remaining pull requests"
                    res=1
                else
                    debug "Skipping $(cat $repo/github_url)"
                fi
            done;;
    esac
    if ((res == 0)); then
        debug "Pull request checked out." \
            "You can get back to your regular tree with ./dev switch"
        return 0
    else
        switch_release
        debug "Could not check out pull request."
        return 1
    fi
}

# Have Github merge all the changes in a pull request bundle.
pull_request_merge() {
    # $1 = index
    local id res=0 repo
    id=$(pull_request_number_to_id "$1") || \
        die "$1 is not a pull request number!"
    case $id in
        singletons/*)
            pull_request_merge_one "$OPEN_PULL_REQUESTS/$id"
            res=$?;;
        bundles/*)
            for repo in "$OPEN_PULL_REQUESTS/$id/"*; do
                [[ -d $repo ]] || continue
                if ((res == 0)); then
                    pull_request_merge_one "$repo" && continue
                    debug "Aborting remaining pull requests."
                    res=1
                else
                    debug "Skipping $(cat "$repo/github_url")"
                fi
            done;;
    esac
    if ((res == 0)); then
        debug "Pull requests merged." \
            "You can use $0 fetch to pull in updated pull request information."
        return 0
    else
        debug "Please fix up the broken pull requests and retry the merge."
        return 1
    fi
}

pull_request_comment_one() {
    # $1 = path to pull request
    # $2 = body of comment
    . <(source_prq_vars "$1" local)
    curl_and_res -f -X POST \
        -d "{ \"body\": \"$2\"}" \
        "https://api.github.com/repos/$prq_target_account/$prq_local_repo/issues/$prq_number/comments"
}

# Add a comment to a pull request.
pull_request_comment() {
    # $1 = pull request number
    # $2 = body of comment
    local id repo
    id=$(pull_request_number_to_id "$1") || exit 1
    case $id in
        singletons/*)
            pull_request_comment_one "$OPEN_PULL_REQUESTS/$id" "$2";;
        bundles/*)
            for repo in "$OPEN_PULL_REQUESTS/$id/"*; do
                [[ -d $repo ]] || continue
                pull_request_comment_one "$repo" "$2"
            done;;
    esac
}

pull_request_close_one() {
    # $1 = path to local pull request info
    # $2 = reason for closing the pull request
    pull_request_comment_one "$1" "$2" || return 1
    . <(source_prq_vars "$1" local)
    curl_and_res -f -X PATCH \
        -d "{ \"state\": \"closed\"}" \
        "https://api.github.com/repos/$prq_target_account/$prq_local_repo/pulls/$prq_number"
}

# Close a pull request without merging it.
pull_request_close() {
    # $1 = pull request number
    # $2 = closing comment
    local id repo
    id=$(pull_request_number_to_id "$1") || exit 1
    case $id in
        singletons/*) pull_request_close_one "$OPEN_PULL_REQUESTS/$id" "$2";;
        bundles/*)
            for repo in "$OPEN_PULL_REQUESTS/$id/"*; do
                [[ -d $repo ]] || continue
                pull_request_close_one "$repo" "$2"
            done;;
    esac
}

# Helper function for trying to do something with the CI system and push out the results.
# The thing to do should be idempotent.
ci_do_and_push() {
    while true; do
        "$@" || return $?
        push_ci_tracking
        local __res=$?
        case $__res in
            0) return 0;;
            1) continue;;
            2) die "No local pull tracking repository!";;
            3) die "Could not find remote to push CI tracking to!";;
            128) die "Could not talk to upstream CI tracking remote!";;
            *) return $__res;;
        esac
    done
}

# Fetch CI tracking information and try to merge it with any local information
# that has not been pushed upstream.  If we cannot merge, we throw away the
# local changes and let the caller know that we failed to merge local changes.
fetch_ci_tracking() (
    [[ ! -d $LOCAL_PULL_TRACKING ]] && mkdir -p "$LOCAL_PULL_TRACKING"
    if [[ ! -d $LOCAL_PULL_TRACKING/.git ]]; then
        for remote in "${DEV_SORTED_REMOTES[@]}"; do
            remote_available "$remote" || continue
            test_remote "${DEV_REMOTE_URLBASE[$remote]}/$CI_TRACKING_REPO" || continue
            git clone -o "$remote" \
                "${DEV_REMOTE_URLBASE[$remote]}/$CI_TRACKING_REPO" \
                "$LOCAL_PULL_TRACKING" || return 2
            return 0
        done
        return 2
    fi
    cd "$LOCAL_PULL_TRACKING"
    git fetch -q --all || return 128
    quiet_checkout -f master
    remote=$(git config --get branch.master.remote) || return 3
    exec &>/dev/null
    if branches_synced "." "$remote/master" "master"; then
        git reset --hard "$remote/master"
        git clean -f -d
        return 0
    fi
    git rebase "$remote/master" master && return 0
    git rebase --abort
    git reset --hard "$remote/master"
    git clean -f -d
    return 1
)

# Push CI tracking to the upstream repository.
push_ci_tracking() (
    [[ $SKIP_CI_PUSH ]] && return
    local remote
    fetch_ci_tracking || return $?
    cd "$LOCAL_PULL_TRACKING"
    remote=$(git config --get branch.master.remote) || return 3
    git push -q "$remote" "master:master"
    local __res=$?
    (( $__res > 127 || $__res == 0 )) && return $res
)

# Get all the IDs that the pull request system knows about.
ci_ids() {
    local id ids=()
    ids=("$LOCAL_PULL_TRACKING/bundles/"* "$LOCAL_PULL_TRACKING/singletons/"*/*/*)
    for id in "${ids[@]}"; do
        echo "${id#$LOCAL_PULL_TRACKING/}"
    done
}

ci_filtered_ids() {
    local id state test_state
    local -a ids states
    while read id; do
        states=()
        while read state; do
            states+=("$state")
        done < <(ci_get_current_states "$id")
        for state in "${states[@]}"; do
            for test_state in "$@"; do
                [[ $test_state = $state* ]] || continue
                ids+=("$id")
                break 2
            done
        done
    done < <(ci_ids)
    __random_order "${ids[@]}"
}

# Get all the pull request IDs that are not closed or merged.
ci_open_ids() {
    local id
    local -a ids
    while read id; do
        local state=$(ci_get_current_states "$id")
        [[ $state = closed || $state = merged || \
            $state = failed || $state = needs-work ]] && continue
        ids+=("$id")
    done < <(ci_ids)
    __random_order "${ids[@]}"
}

__ci_set_state() (
    # $1 = id
    # $2 = state
    # $3 = comment
    cd "$LOCAL_PULL_TRACKING/$1" || die "$1 is not being tracked by CI"
    state="ci_state/$2"
    mkdir -p "$state"
    echo "$3" >>"$state/comment"
    git add "$state"
    git commit -m "Added state $2 to $1"
)

# Find the states that the current state transitioned from.
ci_last_states() {
    # $1 = pull request ID
    # $2 = state
    [[ -d $LOCAL_PULL_TRACKING/$1 ]] || \
        die "$1 is not a CI tracked pull request!"
    local state_dir="$LOCAL_PULL_TRACKING/$1/ci_state/$2"
    [[ -d $state_dir ]] || \
        die "$2 is not a state in $1"
    [[ -d $state_dir/last_states ]] || return 0
    local sfile state
    while read -r state; do
        state=${state#./}
        state=${state%/.last}
        echo "$state"
    done < <(cd "$state_dir/last_states"; find -name .last -type f)
}

__ci_state_walker() {
    local state this_states=()
    while read -r state; do
        [[ ${states[$state]} ]] && continue
        states["$state"]="$3"
        this_states+=("$state")
    done < <(ci_last_states "$1" "$2")
    [[ $this_states ]] || return 0
    for state in "${this_states[@]}"; do
        __ci_state_walker "$1" "$state" "$(( ${3} + 1))"
    done
}

ci_all_last_states() {
    # $1 = pull request ID
    # $2 = starting state
    local -A states
    __ci_state_walker "$1" "$2" 0
    (( ${#states[@]} == 0 )) && return 0
    printf "%s\n" "${!states[@]}"
}

# Link an old state to a new one.  This function is responsible for
# making sure that the overall state transition graph remains sane.
ci_link_state() {
    # $1 = pull request ID
    # $2 = old state
    # $3 = new state
    [[ -d $LOCAL_PULL_TRACKING/$1/ci_state ]] || \
        die "$1 is not being tracked by the CI system!"
    local state_dir="$LOCAL_PULL_TRACKING/$1/ci_state"
    [[ -d $state_dir/$2 ]] || \
        die "$1 does not have old state $2"
    [[ -d $state_dir/$3 ]] || \
        die "$1 does not have new state $3"
    [[ $2 != $3 ]] || \
        die "$1: Cannot link $2 to itself!"
    local state
    # Check to see if this link already exists.
    # If it does, we just succeed without doing anything.
    [[ -f $state_dir/$3/last_states/$2 ]] && return 0
    # Check to see if adding this link would make the graph cyclical.
    # If it will, just die.
    while read -r state; do
        [[ $state = $3 ]] && \
            die "$1: Linking $2 to $3 would create a cyclical state graph!"
    done < <(ci_all_last_states "$1" "$2")
    mkdir -p "$LOCAL_PULL_TRACKING/$1/ci_state/$3/last_states/$2"
    touch "$LOCAL_PULL_TRACKING/$1/ci_state/$3/last_states/$2/.last"
    (   cd "$LOCAL_PULL_TRACKING/$1/ci_state"
        git add "$3/last_states/$2/.last"
        git commit -m "Made state $3 a child of $2 in $1")
}


# Figure out the set of possible next states from the current state.
# This may not be the same as the next states we will wind up transitioning to.
# It is also the reference for what states the CI state machine can be in.
ci_all_next_states() {
    # $1 = index or local ID.
    # $2 = current state
    local -A unit_test_whitelist
    local -a next
    local id release build os oses current_state
    # As we get more releases that are unit test enabled, add them here,
    # replace this with blacklist logic or (ideally) remove it altogether.
    unit_test_whitelist["development"]=true
    id=$(pull_request_number_to_id "$1") || \
        die "$1 is not a valid pull request!"
    [[ -d $LOCAL_PULL_TRACKING/$id ]] || \
        die "Pull request $1 has not been imported into CI." \
        "Please run $0 ci import."
    if [[ $2 ]]; then
        current_state="${2}"
    else
        local states=()
        while read current_state; do
            states+=("$current_state")
        done < <(ci_get_current_states "$id")
        if (( ${#states[@]} > 1)); then
            die "Pull request $1 is in more than one state:" \
                "${states[@]}" \
                "Please pass one of these to next-states"
        fi
        current_state="${states[0]}"
    fi
    read -r release < "$LOCAL_PULL_TRACKING/$id/release"
    case $current_state in
        # The first two states are fairly simple in their allowed transitions.
        new) next=(merge-testing);;
        merge-testing) next=(merge-tested);;
        # Unit testing happens on a per-release/build basis,
        # and is not applicable to all releases.  Our next state
        # depends on the whitelist.
        merge-tested)
            while read build oses; do
                build="${build%:}"
                if [[ ${unit_test_whitelist["$release"]} && $build = $release/master ]]; then
                    next+=("unit-testing $build")
                else
                    for os in $oses; do
                        next+=("build-testing $build $os")
                    done
                fi
            done < <(builds_for_pull_request "$id");;
        # unit testing happens on a per-release and per-build basis.
        unit-testing*)
            build="${current_state#* }"
            next=("unit-tested $build");;
        unit-tested*)
            b="${current_state#* }"
            while read build oses; do
                build=${build%:}
                if [[ $build != $b ]]; then continue; fi
                for os in $oses; do
                    next+=("build-testing $build $os")
                done
            done < <(builds_for_pull_request "$id");;
        # Build and smoke tests happen on a release/build/os basis.
        build-testing*)
            build="${current_state#* }"
            next=("build-tested $build");;
        build-tested*)
            build="${current_state#* }"
            next=("smoke-testing $build");;
        smoke-testing*)
            build="${current_state#* }"
            next=("smoke-tested $build");;
        smoke-tested*) next=(code-reviewing);;
        # code-reviewing can actaully happen in parallel with the above
        # steps.
        code-reviewing) next=(code-reviewed);;
        code-reviewed) next=(mergeable);;
        # Mergeable cannot happen until we have a sufficient number of code-reviewed states
        # and we have smoke-tested states for all the release/build/OS combos applicable.
        mergeable) next=(merged);;
        failed|needs-work) next=(new);;
        # Merged and closed are terminal states.
        # Nothing else can happen to them.
        merged|closed) next=();;
        *) die "Unknown state $current_state!";;
    esac
    [[ $next ]] && printf "%s\n" "${next[@]}"
}

# Print any passed args in pseudo-random order
__random_order() {
    local -a res
    local k v
    for v in "$@"; do
        k="$RANDOM"
        while [[ ${res[$k]} ]]; do k="$RANDOM"; done
        res[$k]="$v"
    done
    printf "%s\n" "${res[@]}"
}

# Wrapper around ci_all_next_states that filters out next states that have
# an intent registered for them, and prints them in random order.
ci_next_states() {
    local state id state_dir
    local -a states
    id=$(pull_request_number_to_id "$1") || \
        die "$1 is not a valid pull request!"
    [[ -d $LOCAL_PULL_TRACKING/$id ]] || \
        die "Pull request $1 has not been imported into CI." \
        "Please run $0 ci import."
    state_dir="$LOCAL_PULL_TRACKING/$id/ci_state/$2"
    while read -r state; do
        [[ -d $state_dir/intents/$state/timeout ]] && continue
        states+=("$state")
    done < <(ci_all_next_states "$id" "$2")
    __random_order "${states[@]}"
}

# Get the current states that a given pull request in the CI is currently in.
# In the interest of parallelization, a pull request can be in multiple CI
# states at any given time.
ci_get_current_states() {
    # $1 = pull request ID.
    local state_dir state last_state last_state_file id
    local -A states
    id=$(pull_request_number_to_id "$1") || \
        die "$1 is not a valid pull request!"
    [[ -d $LOCAL_PULL_TRACKING/$id/ci_state ]] || die "$id is not being tracked by CI"
    # First, read all of the state paths in.
    while read state_dir; do
        state_dir="${state_dir%/comment}"
        state_dir="${state_dir%/last_states}"
        states[${state_dir##*/ci_state/}]=current
    done < <(find "$LOCAL_PULL_TRACKING/$id/ci_state/" -type f -and -name comment -or -type d -and -name last_states)
    # Second, mask out all of the states that have a last file pointing at them.
    # This needs to be ornate-ified to handle per-state special conditions.
    for state in "${!states[@]}"; do
        # If this state has no last_states, then it is current.
        [[ -d $LOCAL_PULL_TRACKING/$id/ci_state/$state/last_states || $state = new ]] || continue
        # If we have intents, and some of them are unfulfilled,
        # we are not stale.
        if [[ -d $LOCAL_PULL_TRACKING/$id/ci_state/$state/intents ]]; then
            local next_state
            while read -r next_state; do
                next_state=${next_state##*/intents/}
                next_state=${next_state%/timeout}
                next_state=${next_state%/.intent}
                # if $next_state is not a directory under ci_state,
                #then this intent is not filled.
                [[ -d $LOCAL_PULL_TRACKING/$id/ci_state/$next_state ]] && continue
                states["$state"]=unfilled
                continue 2
            done < <(find "$LOCAL_PULL_TRACKING/$id/ci_state/$state/intents" -name timeout -or -name .intent)
            states["$state"]=stale
        fi
        while read -r last_state; do
            # Sanity-check the state transition graph.
            [[ ${states[$last_state]} ]] || \
                die "State graph for CI item $id is invalid." \
                "State $state says that it transitioned from $last_state," \
                "but $last_state is not in the state graph!"
            # If any of the last states have intents, and those intents are not filled,
            # then don't set that last state as stale.
            [[ ${states["$last_state"]} = unfilled ]] && continue
            # If we get this far, $last_state is stale.
            states[$last_state]=stale
        done < <(ci_last_states "$id" "$state")
    done
    # Anything not marked as stale is a state that we are in.
    local -A active_states
    for state in "${!states[@]}"; do
        [[ ${states["$state"]} = stale ]] && continue
        active_states["$state"]="${states[$state]}"
    done
    # See if we are in one of the terminal states
    for last_state in closed failed needs-work merged new; do
        for state in "${!active_states[@]}"; do
            [[ $state = $last_state ]] || continue
            echo "$state"
            return
        done
    done
    __random_order "${!active_states[@]}"
}

ci_kill_stale_intents() (
    # $1 = pull request ID
    [[ -d $LOCAL_PULL_TRACKING/$1/ci_state ]] || return 1
    cd "$LOCAL_PULL_TRACKING/$1/ci_state"
    local intent need_commit=false
    while read intent; do
        [[ $intent = */intents/* ]] || continue
        intent=${intent%/timeout}
        local from=${intent%/intents/*}
        from="${from#./}"
        local to=${intent#*/intents/}
        [[ -d $to ]] && continue
        (( $(cat "$intent/timeout") >= $(date -u '+%s'))) && continue
        [[ -f $intent/.intnet ]] || {
            touch "$intent/.intent"
            git add "$intent/.intent"
        }
        (cd "$intent"; git rm -f timeout +([[:xdigit:]])) &>/dev/null
        need_commit=true
    done < <(find -name timeout -type f)
    [[ $need_commit = true ]] || return 1
    git commit -q -m "Killed stale intents for $1"
)

ci_register_intent() {
    # $1 = pull request ID
    # $2 = current state
    # $3 = intended next state
    # $4 = timeout for intent.  Defaults to 5 hours.
    local timeout="${4:-18000}" id prq state state_ok=false
    id=$(pull_request_number_to_id "$1") || \
        die "$1 is not a valid pull request!"
    [[ -d $LOCAL_PULL_TRACKING/$id ]] || \
        die "Pull request $1 has not been imported into CI." \
        "Please run $0 ci import."
    prq="$LOCAL_PULL_TRACKING/$id"
    [[ -d $prq/ci_state/$2 ]] || {
        debug "$id does not have state $2, cannot register an intent to move to $3"
        return 1
    }
    [[ -d $prq/ci_state/$3 ]] && {
        debug "$id already has state $3"
        return 1
    }
    state_ok=false
    while read state; do
        [[ $state = $3 ]] || continue
        state_ok=true
        break
    done < <(ci_all_next_states "$id" "$2")
    [[ $state_ok = true ]] || \
        die "$id: State $2 does not have a next state of $3, cannot register an intent."
    # See if we need to expire intents on the current state.
    if [[ -f $prq/ci_state/$2/intents/$3/timeout ]]; then
        [[ $timeout = 0 ]] && return 1
        ci_kill_stale_intents "$id" && push_ci_tracking && \
            [[ ! -f $prq/ci_state/$2/intents/$3/timeout ]] || return 1
    fi
    mkdir -p "$prq/ci_state/$2/intents/$3/"
    local intent_id=$((ip addr show; date -u "+%s%N"; printf "$RANDOM") |sha1sum -)
    intent_id="${intent_id%% *}"
    echo "$(( $(date -u '+%s') + ${timeout} ))" > "$prq/ci_state/$2/intents/$3/timeout"
    touch "$prq/ci_state/$2/intents/$3/$intent_id"
    touch "$prq/ci_state/$2/intents/$3/.intent" # Solely to keep git from nuking the dir.
    (   cd "$prq/ci_state/$2/intents/$3/"
        git add .
        git commit -m "Registered intent to transition $id from $2 to $3") &>/dev/null
    echo "$intent_id"
}

ci_handle_intent() {
    # $1 = intent ID
    # $2 = passed or failed
    local -a intents states
    local intent state
    while read -r intent; do
        intents+=("$intent")
    done < <(find "$LOCAL_PULL_TRACKING/singletons" "$LOCAL_PULL_TRACKING/bundles" \
        -name "$1" -type f) 2>/dev/null
    [[ ${intents[1]} ]] && \
        die "More than one intent with id $1 found. This should never happen." \
        "${intents[@]}"
    [[ ${intents[0]} ]] || \
        die "Intent $1 not found."
    intent="${intents[0]#$LOCAL_PULL_TRACKING/}"
    local prq="${intent%%/ci_state/*}"
    intent="${intent#${prq}/ci_state/}"
    local current_state=${intent%/intents/*}
    local next_state=${intent#*/intents/}
    next_state=${next_state%/*}
    case $2 in
        failed)
            [[ $(ci_get_current_states "$prq") = failed ]] && return 0
            while read state; do
                states+=("$state")
            done  < <(ci_get_current_states "$prq")
            __ci_set_state "$prq" failed \
                "Intent to transition from $current_state to $next_state failed."
            for state in "${states[@]}"; do
                ci_link_state "$prq" "$state" failed
            done;;
        passed)
            __ci_set_state "$prq" "$next_state" \
                "Intent to transition from $current_state to $next_state passed."
            ci_link_state "$prq" "$current_state" "$next_state";;

        expire) (cd "$LOCAL_PULL_TRACKING/$prq/ci_state/$current_state/intents/$next_state/"
                [[ -f .intent ]] || {
                    touch .intent
                    git add .intent
                }
                git rm -f timeout +([[:xdigit:]])
                git commit -m "Expired intent to transition $prq from $current_state to $next_state"
            );;
        delete) (cd "$LOCAL_PULL_TRACKING/prq/ci_state"
                git rm -rf "$current_state/intents/$next_state/"
                git commit -m "Deleted intent to transition $prq from $current_state to $next_state");;
        *) die "Second arg to ci_handle_intent should either be passed or failed.";;
    esac
}

ci_commit_intent() {
    ci_do_and_push ci_handle_intent "$1" "passed" || return $?
    pull_request_comment "$prq" "CI: Gating tests for $current_state to $next_state passed."
}
ci_fail_intent() {
    ci_do_and_push ci_handle_intent "$1" "failed" || return $?
    pull_request_comment "$prq" "ci: gating tests for $current_state to $next_state failed."
}
ci_expire_intent() { ci_do_and_push ci_handle_intent "$1" "expire"; }
ci_delete_intent() { ci_do_and_push ci_handle_intent "$1" "delete"; }

# Show all incomplete intents for a CI ID.
ci_show_intents() (
    local id intent
    local -A intents
    id=$(pull_request_number_to_id "$1") || \
        die "$1 is not a valid pull request!"
    cd "$LOCAL_PULL_TRACKING/$id/ci_state" || die "$id is not in the CI system"
    while read -r intent; do
        [[ $intent = */intents/* ]] || continue
        intent=${intent#./}
        intent=${intent%/*}
        from_state=${intent%%/intents/*}
        to_state=${intent##*/intents/}
        [[ -d $to_state ]] && continue
        [[ ${intents["$intent"]} ]] && continue
        intents["$intent"]="$from_state => $to_state: "
        if [[ -f $intent/timeout ]] && (( $(date -u +%s) <= $(cat "$intent/timeout") )); then
            intents["$intent"]+="active, id=$(cd "$intent"; echo +([[:xdigit:]]))"
        else
            intents["$intent"]+="stale"
        fi
    done < <(find -name .intent -or -name timeout)
    printf "%s\n" "${intents[@]}"
)

# Handle transitioning a pull request in the CI state machine to closed
# or merged once it has been closed on Github.
ci_close_stale_pull_request() (
    local prq prqs=() p_closed p_merged cout p_state
    local -A pull
    # Don't close it until we positively confirm that the pull request is closed.
    cd "$LOCAL_PULL_TRACKING"
    case $1 in
        singletons/*) prqs+=("$LOCAL_PULL_TRACKING/$1");;
        bundles/*)
            for prq in "$LOCAL_PULL_TRACKING/$1/"*; do
                [[ -d $prq && -f $prq/state ]] || continue
                prqs+=("$prq")
            done;;
        *) die "Cannot happen!"
    esac
    local p_closed=true
    local p_merged="merged"
    for prq in "${prqs[@]}"; do
        local cout=""
        . <(source_prq_vars "$prq" local)
        cout=$(curl_and_res -X GET \
            "https://api.github.com/repos/$prq_target_account/$prq_local_repo/pulls/$prq_number")
        (( $? == 0 )) || return
        local -A pull
        . <(parse_yml_or_json - pull <<< "$cout")
        [[ ${pull["state"]} = closed || ${pull["message"]} = 'Not Found' ]] || return
        [[ ${pull["merged"]} && ${pull["merged"]} = true ]] || p_merged="closed"
    done
    debug "Setting state of pull request $1 to $p_merged"
    __ci_set_state "$1" "$p_merged" "Upstream pull request $p_merged."
    for p_state in "${states[@]}"; do
        ci_link_state "$1" "$p_state" "$p_merged"
    done
    git add "$1"
    git commit --allow-empty -qm "Pull request id $1 $p_merged upstream."
)

ci_close_stale_pull_requests() {
    local id commit prqs=() prq
    local -A states

    for prq in "$LOCAL_PULL_TRACKING/bundles/"* "$LOCAL_PULL_TRACKING/singletons/"*/*/*; do
        id="${prq#$LOCAL_PULL_TRACKING/}"
        [[ -d $prq/ci_state ]] || continue
        [[ -d $prq/ci_state/closed || -d $prq/ci_state/merged ]] && continue
        [[ -d $OPEN_PULL_REQUESTS/$id ]] && continue
        ci_close_stale_pull_request "$id"
    done
}

ci_reset_pull_request() {
    local id
    id=$(pull_request_number_to_id "$1") || \
        die "$1 is not a valid pull request!"
    [[ -d $LOCAL_PULL_TRACKING/$id ]] || \
        die "$id is not being tracked in the CI system, cannot reset it!"
    case $(ci_get_current_states "$id") in
        new) return 0;;
        closed|merged) die "$id is already closed or merged, cannot be reset!";;
    esac
    cp -a "$OPEN_PULL_REQUESTS/$id/." "$LOCAL_PULL_TRACKING/$id/."
    (   cd "$LOCAL_PULL_TRACKING/$id"
        git rm -rf ci_state
        __ci_set_state "$id" "new" "State machine reset due to pull request update."
        git add .
        git commit -q -m "State machine for $id reset by $DEV_GITHUB_ID"
    )
}

# Test to see if a pull request needs to be reset back to the new
# state due to the underlying branches changing.
ci_maybe_reset_pull_request() {
    # $1 = pull request ID
    local prqs=() prq tag needs_reset=false
    case $1 in
        singletons/*) prqs=("$1");;
        bundles/*)
            for prq in "$LOCAL_PULL_TRACKING/$1/"*; do
                [[ -d $prq && -f $prq/state ]] || continue
                prqs+=("${prq#$LOCAL_PULL_TRACKING/}")
            done;;
    esac
    for prq in "${prqs[@]}"; do
        for tag in source_sha target_sha; do
            # If only some of them are here, this pull request is probably
            # in a partially-closed state.  Ignore it until we think of something
            # better to do.
            [[ -f $OPEN_PULL_REQUESTS/$prq/$tag && \
                -f $LOCAL_PULL_TRACKING/$prq/$tag ]] || continue
            [[ $(cat "$OPEN_PULL_REQUESTS/$prq/$tag") = \
                $(cat "$LOCAL_PULL_TRACKING/$prq/$tag") ]] && continue
            needs_reset=true
            break 2
        done
    done
    [[ $needs_reset = false ]] && return 0
    ci_reset_pull_request "$1"
}

# Handle cleaning up old state transitions, resetting any pulls that need
# resetting and importing any pull requests we are not tracking.
ci_import_new_pull_requests() {
    local nr id
    [[ -f $OPEN_PULL_REQUESTS/pull_request_index ]] || return 0
    ci_close_stale_pull_requests
    while read nr id; do
        if [[ -d $LOCAL_PULL_TRACKING/$id ]]; then
            ci_maybe_reset_pull_request "$id"
            continue
        fi
        mkdir -p "$LOCAL_PULL_TRACKING/$id"
        cp -a "$OPEN_PULL_REQUESTS/$id/." "$LOCAL_PULL_TRACKING/$id/."
        __ci_set_state "$id" "new"
        debug "Adding new pull request $id"
        (   cd "$LOCAL_PULL_TRACKING/$id"
            git add .
            git commit -qm "Added new pull request $id") &>/dev/null
    done < "$OPEN_PULL_REQUESTS/pull_request_index"
}

__fetch_all() {
    local remote remotes=() res=true
    if [[ $@ || $DEV_FROM_REMOTES ]]; then
        [[ $1 ]] && remotes+=("$@")
        [[ $DEV_FROM_REMOTES ]] && remotes+=("${DEV_FROM_REMOTES[@]}")
    else
        remotes=("${DEV_SORTED_REMOTES[@]}")
    fi
    local logfile="${PWD}"
    logfile=${logfile##$(readlink -f "$CROWBAR_DIR")}
    logfile=${logfile//\//-}
    logfile=${logfile#-}
    logfile=${logfile//barclamps/barclamp}
    [[ $logfile ]] || logfile="Crowbar"
    for remote in "${remotes[@]}"; do
        crowbar_remote_exists "$remote" && \
            remote_available "$remote" && \
            git_remote_exists "$remote" || \
            continue
        if { git fetch -q "$remote" && git fetch -q -t "$remote"; } &>/dev/null; then
            debug "  Fetched from $remote." 2>>"$results_dir/$logfile"
            printf '.'
            if [[ $PWD = */barclamps/* ]]; then

                fetch_pull_request_metadata "$remote" "barclamp-${PWD##*/}"
            else
                fetch_pull_request_metadata "$remote" "crowbar"
            fi
        else
            debug "  Failed to fetch from $remote" 2>>"$results_dir/$logfile"
            printf '!'
            res=false
        fi
    done
    scrub_merged_pull_requests
    [[ $res = true ]] && debug "  All fetches passed." 2>> "$results_dir/$logfile"
}

# Fetch (but do not merge) updates from all our remotes, in both the
# main Crowbar repository and the barclamps.
fetch_all() {
    local results_dir d
    clear_pull_request_metadata
    dt=$(date +%s)
    results_dir="$(mktemp -d /tmp/crowbar_fetch_${USER}_${dt}_XXXXXX)" || \
        die "Cannot create temporary storage for fetch results!"
    fetch_ci_tracking
    debug "Fetching updates:"
    trap "kill -TERM -$$ && rm -rf $results_dir ; exit 1" INT QUIT
    for d in "$CROWBAR_DIR/barclamps/"* "$CROWBAR_DIR"; do
        [[ -d $d/.git ]] || continue
        if [[ $BADLINK ]]; then
          cd "$d" && __fetch_all "$@"
        else
          ( cd "$d" && __fetch_all "$@") &
        fi
    done
    wait
    echo
    for d in "$results_dir"/*; do
        local n="${d##*/}"
        n="${n//-/ }"
        if grep -q 'All fetches passed\.$' "$d"; then
            debug "${n}: All updates fetched"
        else
            debug "${n}:"
            cat "$d"
        fi
    done
    if [[ $BADLINK ]]; then
      debug "not removing $results_dir"
    else
      rm -rf "$results_dir"
    fi
    update_all_tracking_branches
    show_open_pull_requests
    sort_pull_requests
    if [[ -d $LOCAL_PULL_TRACKING/.git ]]; then
        ci_import_new_pull_requests
    fi
}

# Attempt to scrub merged pull requests across all of the barclamps.
scrub_merged_pulls() {
    in_repo scrub_merged_pull_requests "$@"
    for barclamp in "$CROWBAR_DIR/barclamps/"*; do
        [[ -d $barclamp ]] || continue
        in_barclamp "${barclamp##*/}" scrub_merged_pull_requests "$@"
    done
}

# Helper function for calling curl to talk to github.
curl_and_res() {
    local __r
    __r="$(curl -n "$@" 2>/dev/null)"
    case $? in
        0) printf '%s' "$__r";;
        7) echo "Unable to contact Github, please try again later." >&2
            return 1;;
        22) return 2;;
        *) echo "Curl reported error ${?}!." >&2
            return 3;;
    esac
}

# Check to see if a repository exists on Github using the Github API.
github_repo_exists() {
    # $1 = repo to check for
    # $2 = user to check for it in.  Defaults to $DEV_GITHUB_ID
    local repo=$1 user=${2-$DEV_GITHUB_ID}
    curl_and_res -f -X GET "https://api.github.com/repos/$user/$repo" &>/dev/null
}

# Fork a repository on Github.
github_fork() {
    # $1 = user to fork from
    # $2 = repo to fork
    curl_and_res -f -X POST \
        "https://api.github.com/repos/$1/$2/forks" >&/dev/null || \
        die "Could not fork $1/$2! (your .netrc file may be missing)"
}

git_url_for_remote() { git_remote_exists "$1" && git config --get "remote.$1.url"; }

# Get the name of this repo based on the URL of the origin remote.
get_repo_name() {
    local repo
    repo="$(git_url_for_remote $(origin_remote))" || \
        repo="$(git_url_for_remote origin)" || \
        die "$PWD: Could not get the URL for the origin remote for this repo."
    repo="${repo##*:}"
    echo "${repo##*/}"
}

git_remote_exists() { git_config_has "remote.$1.url"; }
crowbar_remote_exists() { in_repo git_config_has "crowbar.remote.$1.urlbase"; }

# Add a git remote to a repository.
add_remote() {
    # $1 = name of the remote to add.
    # $2 = base part of the repo to add.
    # $3 = Name of the remote repository to add.
    git_remote_exists "$1" && return 0
    local repo
    repo="${3:-$(get_repo_name)}" || exit 1
    repo="$2/$repo"
    git ls-remote "$repo" refs/heads/master &>/dev/null || {
        debug "No git repo at $repo, skipping."
        return 0
    }
    git remote add "$1" "$repo"
    git fetch "$1"
}

rm_remote() { git_remote_exists "$1" && git remote rm "$1"; }
rename_remote() { git_remote_exists "$1" && git remote rename "$1" "$2"; }

# Change the URL that a remote points at.
set_url_remote() {
    # $1 = name of the remote
    # $2 = new baseurl for the remote
    # $3 = Name of the remote repository.
    git_remote_exists "$1" || return 0
    local repo
    repo="${3:-$(get_repo_name)}" || exit 1
    repo="$2/$repo"
    git ls-remote "$repo" refs/heads/master &>/dev/null || {
        debug "No git repo at $repo, skipping."
        return 1
    }
    git remote set-url "$1" "$repo"
    git fetch "$1"
}


# Synchronize a remote definition as closely as possible
# This assumes that you are already in the repo you want to sync.
__sync_remotes() {
    # $1 = function to call to test to see remote is already valid.
    # $2 = function to call to get the proper remote URL.
    local remote urlbase remote_repo barclamp_repo
    for remote in "${DEV_SORTED_REMOTES[@]}"; do
        urlbase="${DEV_REMOTE_URLBASE[$remote]}"
        [[ $urlbase ]] || continue
        # Do nothing if this remote is correct, otherwise set/add as appropriate.
        remote_repo=$(git_url_for_remote "$remote") && \
            $1 ${remote_repo%.git} && continue
        remote_repo="$($2 "$urlbase")" || continue
        remote_repo="${remote_repo#$urlbase/}"
        debug "Synchronizing remote $remote for $PWD"
        if git_remote_exists "$remote"; then
            set_url_remote "$remote" "$urlbase" "$remote_repo" || continue
        else
            add_remote "$remote" "$urlbase" "$remote_repo" || \
                die "Could not add new remote $remote for $PWD in sync_barclamp_remotes"
        fi
        local update_tracking=true
    done
    if [[ $update_tracking ]]; then
        update_tracking_branches
    fi
}

test_barclamp_remote() {
    # $1 = local repo name
    # $2 = remote repo
    [[  $2 = $urlbase/barclamp-$1 || \
        $2 = $urlbase/crowbar/barclamps/$1 ]]
}

# Synchronize remote definitions in the barclamps to match what is in the
# main Crowbar repository as closely as possible.
sync_barclamp_remotes() {
    in_barclamp "$1" __sync_remotes "test_barclamp_remote $1" "probe_barclamp_remote $1"
}

test_simple_remote() [[ $2 = $urlbase/$1 ]]
get_simple_remote() {
    test_remote "$2/$1" && echo "$2/$1"
}

sync_crowbar_remotes() {
    in_repo __sync_remotes "test_simple_remote crowbar" "get_simple_remote crowbar"
}

sync_ci_remotes() (
    cd "$LOCAL_PULL_TRACKING"
    __sync_remotes "test_simple_remote $CI_TRACKING_REPO" "get_simple_remote $CI_TRACKING_REPO"
)

# Show saved parameters for either a specific remote or all of them.
show_remote() {
    if [[ $1 ]]; then
        [[ ${DEV_REMOTE_PRIORITY[$1]} ]] || \
            die "$1 is not a remote!"
        echo "$1 urlbase=${DEV_REMOTE_URLBASE[$remote]} priority=${DEV_REMOTE_PRIORITY[$remote]}"
        exit 0
    fi
    for remote in "${DEV_SORTED_REMOTES[@]}"; do
        echo "$remote urlbase=${DEV_REMOTE_URLBASE[$remote]} priority=${DEV_REMOTE_PRIORITY[$remote]}"
    done
}

# This function sets up the hashes that we use to handle our remotes.
# It should be called whenever we need to update them, which is usually
# at startup time.
set_sorted_remotes() {
    local line remote
    local remote_re='^crowbar\.remote\.([^.]+)\.(urlbase|priority)=(.*)$'
    while read line; do
        [[ $line =~ $remote_re ]] || continue
        local remote="${BASH_REMATCH[1]}" key="${BASH_REMATCH[2]}"
        local val="${BASH_REMATCH[3]}"
        case $key in
            priority) DEV_REMOTE_PRIORITY[$remote]=$val;;
            urlbase)
                DEV_REMOTE_URLBASE[$remote]=$val
                [[ ${DEV_REMOTE_PRIORITY[$remote]} ]] && continue
                DEV_REMOTE_PRIORITY[$remote]=50;;
            *) die "Cannot happen in sorted_remotes!"
        esac
    done < <(in_repo git config --list |grep '^crowbar\.remote\.')
    for remote in "${!DEV_REMOTE_PRIORITY[@]}"; do
        __remotes[${DEV_REMOTE_PRIORITY[$remote]}]+="$remote "
    done
    DEV_SORTED_REMOTES=(${__remotes[@]})
}

remote_is_github() {
    git_remote_exists "$1" && [[ ${DEV_REMOTE_URLBASE[$1]} =~ $github_re ]]
}

remote_github_account() {
    remote_is_github "$1" || return 1
    echo "${BASH_REMATCH[2]}"
}

# Figure out which internal remote-handling function to call and how,
# and then do it.
remote_wrapper() {
    # $1 = one of "add", "rm", "set-url", "show"
    # $2 = name of remote
    # $3 = base part of the remote URL.  Only used for add and set-url.
    local remote urlbase action bc cfgaction=()
    local need_tracking_update need_remote_resort
    remote="$2"
    urlbase="$3"
    case $1 in
        add)
            action=add_remote
            if [[ ! $urlbase && $remote =~ $github_re ]]; then
                urlbase="$2"
                remote="${BASH_REMATCH[2]}"
            fi
            if crowbar_remote_exists "${remote}"; then
                die "We already have a remote for $remote."
            fi
            cfgaction=("in_repo git config crowbar.remote.${remote}.urlbase $urlbase")
            need_tracking_update=true
            need_remote_resort=true
            ;;
        rm)
            action=rm_remote
            if ! crowbar_remote_exists "${remote}"; then
                die "No remote named $remote to remove."
            fi
            (( ${#DEV_SORTED_REMOTES[@]} == 1)) && \
                die "$1 is your last remote.  Can't remove it."
            cfgaction=("in_repo git config --unset crowbar.remote.${remote}.urlbase")
            need_tracking_update=true
            need_remote_resort=true
            ;;
        rename)
            action=rename_remote
            if ! crowbar_remote_exists "$remote"; then
                die "Cannot rename $remote to $urlbase, $remote does not exist."
            elif crowbar_remote_exists "$urlbase"; then
                die "Cannot rename $remote to $urlbase, $urlbase already exists!"
            fi
            cfgaction=("in_repo git config --rename-section crowbar.remote.${remote} crowbar.remote.${urlbase}")
            in_repo git_config_has "crowbar.backup.${remote}.method" && \
                cfgaction+=("in_repo git config --rename-section crowbar.backup.${remote} crowbar.backup.${urlbase}")
            need_remote_resort=true
            ;;
        set-url)
            action=set_url_remote
            if ! crowbar_remote_exists "${remote}"; then
                die "Cannot set-url for a remote your have not added."
            fi
            cfgaction=("in_repo git config crowbar.remote.${remote}.urlbase $urlbase")
            ;;
        show) shift;
            show_remote "$1"
            return;;
        sync)
            for bc in "$CROWBAR_DIR/barclamps/"*; do
                [[ -d $bc/.git || -f $bc/.git ]] || continue
                 sync_barclamp_remotes "${bc##*/}"
            done
            sync_crowbar_remotes
            sync_ci_remotes
            return
            ;;
        priority)
            crowbar_remote_exists "$2" || \
                die "Remote $2 must be configured before you can set its priority."
            [[ $3 =~ [0-9]+ ]] && (($3 > 0 && $3 <= 100)) || \
                die "Priority must be a number between 1 and 100"
            in_repo git config "crowbar.remote.$remote.priority" "$3"
            set_sorted_remotes
            update_all_tracking_branches
            return;;
        ''|help) die "Please pass one of add, rm, rename, set-url, sync, priority, or show.";;
        *) die "Unknown action $1 for remote.";;
    esac
    for bc in "$CROWBAR_DIR/barclamps/"*; do
        [[ -d $bc/.git || -f $bc/.git ]] || continue
        debug "Barclamp: ${bc##*/}"
        in_barclamp "${bc##*/}" $action "$remote" "$urlbase" "barclamp-${bc##*/}"
    done
    debug "Crowbar:"
    if [[ $action != sync_barclamp_remotes ]]; then
        in_repo $action "$remote" "$urlbase"
    fi
    if [[ -d $LOCAL_PULL_TRACKING/.git ]]; then
        debug "CI Tracking:"
        (cd "$LOCAL_PULL_TRACKING"; $action "$remote" "$urlbase")
    fi
    local c
    for c in "${cfgaction[@]}"; do
        $c
    done
    [[ $need_remote_resort = true ]] && set_sorted_remotes
    [[ $need_tracking_update = true ]] && update_all_tracking_branches
}

# Handle swizzling up the remotes needed to handle migrating from
# dev metadata v1 to metadata v2
migrate_1_to_2() {
    local url_re='^(https?|ssh|git|file)://' url
    url=$(get_repo_cfg "remote.origin.url") || return 0
    [[ $url =~ $url_re ]] || \
        die "Location for origin remote not in canonical form!" \
        "Please use git remote set-url to update it so that it is in URL form" \
        "(starting with protocol://).  See the git-remote man page."
    if [[ $url =~ $github_re ]]; then
        local remote_name="${BASH_REMATCH[2]}"
        local urlpart="https://github.com/${BASH_REMATCH[2]}"
    else
        local remote_name="upstream"
        local urlpart="${url%/crowbar*}"
        urlpart=${urlpart}
    fi
    remote_wrapper add "$remote_name" "$urlpart"
    remote_wrapper priority "$remote_name" 5
    for rb_source in "${!DEV_REMOTE_BRANCHES[@]}"; do
        rb="${DEV_REMOTE_BRANCHES[$rb_source]}"
        [[ $rb_source = origin ]] && rb_source="$remote_name"
        if [[ ${DEV_REMOTE_SOURCES[$rb_source]} ]]; then
            crowbar_remote_exists "$rb_source" && continue
            remote_available "$rb_source" || continue
            remote_wrapper add "$rb_source" "${DEV_REMOTE_SOURCES[$rb_source]}"
        fi
    done
    crowbar_remote_exists origin && remote_wrapper rm origin
    in_repo git_remote_exists origin && in_repo git remote rm origin
    for bc in "${CROWBAR_DIR}/barclamps/"*; do
        [[ -d $bc/.git || -f $bc/.git ]] || continue
        in_barclamp "${bc##*/}" git_remote_exists origin && \
            in_barclamp "${bc##*/}" git remote rm origin
    done
    for bc in $(in_repo git config --list |grep '^crowbar.backup'); do
        in_repo git config --unset "${bc%%=*}"
    done
    unset b rb rb_source DEV_REMOTE_BRANCHES DEV_REMOTE_SOURCES
}

# Wrapper to handle calling any future migration functions correctly.
migrate() {
    # $1 = source revision
    # $2 = target revision
    local src_rev=${1:-0} target_rev=${2:-$DEV_VERSION} i
    if ((src_rev == 0)) && in_repo git_config_has crowbar.dev.version; then
        src_rev=$(get_repo_cfg crowbar.dev.version)
    fi
    ((src_rev == target_rev)) && return
    [[ $DEV_AVAILABLE_REMOTES ]] && {
        echo "dev setup must have access to all your configured remotes to continue."
        echo "DEV_AVAILABLE_REMOTES is set, indicating that some of your remotes are not available."
        echo "dev setup is aborting."
        exit 1
    } >&2
    ((src_rev > target_rev)) && \
        die "Cannot migrate down from $src_rev to $target_rev."
    for ((i=src_rev; i < target_rev; i++)); do
        grep -q 'function' < <(LC_ALL=C type "migrate_${i}_to_$(($i + 1))" 2>/dev/null) || continue
        "migrate_${i}_to_$(($i + 1))" || \
            die "Migration from $src_rev to $target_rev failed at rev $i"
    done
}


# Perform initial setup.  If you make any changes to this function, take
# care to make sure it stays idempotent.
setup() {
    dev_bash_completion
    local p remote release br head
    local -A touched_branches barclamps
    [[ $(in_repo git symbolic-ref HEAD) = refs/heads/master ]] || \
        die "You must be on the master branch in Crowbar to run setup!"
    barclamps_are_clean || \
        die "Crowbar repo must be clean before trying to set things up!"
    # Make sure we have Github login credentials if our upstream remote
    # was cloned from Github.
    migrate "$(get_repo_cfg crowbar.dev.version)" "$DEV_VERSION"
    if remote_is_github "$(origin_remote)" && [[ $1 != '--no-github' ]]; then
        if [[ $DEV_GITHUB_ID ]]; then
            debug "Validating your Github username ($DEV_GITHUB_ID):"
            if curl_and_res -f \
                "https://api.github.com/users/$DEV_GITHUB_ID" &>/dev/null; then
                debug "$DEV_GITHUB_ID is a valid Github user."
            else
                die "Could not validate $DEV_GITHUB_ID with Github." \
                    "Please edit $HOME/.build-crowbar.conf and" \
                    "$HOME/.netrc to ensure your Githib credentials are correct."
            fi
            debug "Validating your password in .netrc:"
            if curl_and_res -f "https://api.github.com/user" &>/dev/null; then
                debug "Password for $DEV_GITHUB_ID OK."
            else
                die "Unable to authenticate as $DEV_GITHUB_ID at Github." \
                    "Please make sure your passwords in $HOME/.netrc are correct."
            fi
        else
            local DEV_GITHUB_PASSWD
            read -p "Enter your Github username: " DEV_GITHUB_ID
            curl_and_res -f \
                "https://api.github.com/users/$DEV_GITHUB_ID" &>/dev/null || \
                die "Could not verify that $DEV_GITHUB_ID is a valid Github user."
            while [[ $p != $DEV_GITHUB_PASSWD || ! $p ]]; do
                [[ $p ]] && echo "Passwords did not match, try again."
                read -s -p "Enter your Github password: " DEV_GITHUB_PASSWD
                echo
                read -s -p "Enter your Github password again: " p
                echo
            done
            curl_and_res -f -u "$DEV_GITHUB_ID:$DEV_GITHUB_PASSWD" \
                https://api.github.com/user &>/dev/null || {
                echo "Unable to authenticate as Github user $DEV_GITHUB_ID." >&2
                die "Please try again when you have Github access."
            }
            for mach in github.com api.github.com; do
                grep -q "^$mach" "$HOME/.netrc" &>/dev/null && continue
                printf "\nmachine %s login %s password %s\n" \
                    "$mach" "$DEV_GITHUB_ID" "$DEV_GITHUB_PASSWD" >> "$HOME/.netrc"
            done
            chmod 600 "$HOME/.netrc"
            printf "DEV_GITHUB_ID=%q\n" "$DEV_GITHUB_ID" >> "$HOME/.build-crowbar.conf"
        fi
    fi
    # Set up a personal remote if needed.
    if [[ $DEV_GITHUB_ID ]] && remote_is_github "$(origin_remote)" && \
        [[ ${BASH_REMATCH[1]} != $DEV_GITHUB_ID ]] && \
        ! crowbar_remote_exists personal; then
        echo "Adding remote for personal fork of crowbar on Github."
        remote_wrapper add personal "https://github.com/$DEV_GITHUB_ID"
        remote_wrapper priority personal 95
    fi
    in_repo git config branch.autosetupmerge true &>/dev/null
    in_repo git config crowbar.backup.method per-remote
    # Set up the rest of our upstream remotes.
    if crowbar_remote_exists personal && \
        ! in_repo git_config_has "crowbar.backup.$(origin_remote).method"; then
        in_repo git config crowbar.backup.$(origin_remote).method remote
        in_repo git config crowbar.backup.$(origin_remote).remote personal
    fi
    clone_barclamps all
    in_repo git config crowbar.dev.version "$DEV_VERSION"
    in_repo git_config_has 'crowbar.build' && return 0
    in_repo git config 'crowbar.build' 'development/master'
    switch_release
}

# Given a release, find the "best" parent release.  This will only
# be called if we don't already have metadata recorded for the
# parent relationship of this release.
find_best_parent() {
    # $1 = release to find the "best" parent of.
    #      If empty, use the release we are currently on.
    local br distance best_distance ref candidate merge_base release
    local best_candidates=()
    if [[ $1 ]]; then
        release_exists "$1" || \
            die "find_best_parent: $1 is not a release"
        release="$1"
    else
        release="$(current_release)"
    fi
    if candidate=$(parent_release "$release"); then
        printf "%s\n" "$candidate"
        return 0
    elif [[ $release != feature/* ]]; then
        echo "$release"
        return 0
    elif in_repo git_config_has "crowbar.releases.$release.parent"; then
        set_parent_release "$(get_repo_cfg "crowbar.releases.$release.parent")" "$release"
        parent_release "$release"
        return 0
    fi
    debug "More than one good candidate for a parent of $release found."
    debug "Please pick the one you want:"
    select candidate in $(all_releases) "None of the above"; do
        case $candidate in
            'None of the above') die "Aborting.";;
            '') continue;;
            *) break;;
        esac
    done
    echo "$candidate" > "$CROWBAR_DIR/releases/$release/parent"
    in_repo git commit -m "Adding parent for $release" "releases/$release/parent"
}


# Create a new release branch structure based on the current state of the
# Crowbar repositories.
cut_release() {
    local new_branch bc

    [[ $1 ]] || die "cut_release: Please specify a name for the new release"

    # Test to see if release exists.
    release_exists "$1" && die "cut_release: Name already exists"
    local can_cut=true
    new_branch="$(release_branch $1)"
    current_release=$(current_release)
    barclamps_are_clean || \
        die "Crowbar repo must be clean before trying to cut a release!"

    clone_release "$current_release" "$1"
    for build in $(builds_in_release "$current_release"); do
        debug "Creating build $build in new release $1"
        for bc in $(barclamps_from_build "$current_release/$build"); do
            br="$(barclamp_branch_for_build "$current_release/$build" "$bc")"
            [[ $br && $br != empty-branch ]] || continue
            in_barclamp "$bc" git branch -f --no-track "$new_branch" "$br"
            set_barclamp_branch_for_build "$1/$build" "$bc" "$new_branch"
        done
    done
    [[ $1 = feature/* ]] && \
        set_parent_release "$1" "$current_release"
    (cd "$(release_cfg_dir "$1")"; git add .)
    __release_update
    if git_managed_cache && ! in_cache branch_exists "$new_branch"; then
        debug "Creating $new_branch for $1 in the build cache"
        if in_cache branch_exists "$(release_branch)"; then
            in_cache git branch "$new_branch" "$(release_branch)"
        else
            in_cache git branch "$new_branch" master
        fi
    fi
    debug "$1 created."
    switch_release "$1"
}

# Test repository $1 to see if commit $2 is in sync with $3.
# In this case, "in sync" is defined as:
#  * $2 and $3 point at the same commit, or
#  * There are no commits in the set of all commits reachable from $3 that
#    are not also reachable from $2.
branches_synced() {
    # $1 = repository to operate in
    # $2 = local branch to test
    # $3 = remote branch to test
    [[ -d $1/.git || -f $1/.git ]] || \
        die "branches_synced: $1 is not a git repo"
    [[ $VERBOSE2 ]] && echo "Checking to see if out of sync: $2 $3"
    (cd "$1"; git rev-parse --verify -q "$2" &>/dev/null) || \
        return 1
    (cd "$1"; git rev-parse --verify -q "$3" &>/dev/null) || \
        return 1
    # $2 and $3 resolve to the same commit, they are in sync.
    (cd "$1"; [[ $(git rev-parse "$2") = $(git rev-parse "$3") ]] ) && return 0
    # Test to see if there are any commits in $3 that are not
    # reachable from $2.  If there are, then the branches are not synced.
    (cd "$1"; [[ ! $(git rev-list "$2..$3") ]] ) && return 0
    return 1
}

# Erase a release.  Complains if it is not merged into its parent release.
erase_release() {
    # $1 = release refix
    local bc build whine=false current_br parent_br template_br
    local -A branches
    [[ $2 ]] && die "erase-feature only takes one argument"
    [[ $1 = development ]] && die "Cannot erase the development release."
    release_exists "$1" || die "$1 is not a release we can erase!"
    [[ $1 = $(current_release) ]] && die "Cannot erase the release you are on!"
    parent=$(find_best_parent "$1")
    template_br=$(release_branch "$1")
    for build in $(builds_in_release "$1"); do
        if ! build_exists "$parent/$build"; then
            debug "$build does not exist in $parent release."
            whine=true
            continue
        fi
        for bc in $(barclamps_from_build "$1/$build"); do
            if ! __barclamp_exists_in_build "$parent/$build/$bc"; then
                debug "$barclamp does not exist in $parent/$build"
                whine=true
                continue
            fi
            current_br=$(barclamp_branch_for_build "$1/$build" "$bc")
            [[ $current_br && $current_br != empty-branch ]] || continue
            parent_br=$(barclamp_branch_for_build "$parent/$build" "$bc")
            if [[ ! $parent_br || $parent_br = empty-branch ]]; then
                debug "$1/$build/$bc: branch ${release_refs[$bc]} is unique to $1."
                whine=true
            fi
            if [[ $current_br != $template_br ]]; then
                debug "Barclamp $bc is on $current_br, which is not the expected branch name." \
                    "We expected it to be on $template_br"
            fi
            if ! in_barclamp "$bc" branches_synced . "$parent_br" "$current_br"; then
                debug "barclamp $bc: $current_br is not merged into $parent_br"
                whine=true
            fi
        done
    done
    if [[ $whine = true ]]; then
        printf "$1 is not merged into $parent.  Really erase? (y/n): " >&2
        read -n 1
        [[ $REPLY != 'y' ]] && exit
    fi
    debug "Erasing branches for release $1"
    for bc in $(barclamps_in_release "$1"); do
        for current_br in $(barclamp_branches_for_release "$1" "$bc"); do
            in_barclamp "$bc" git branch -D "${current_br}" &>/dev/null
        done
    done
    debug "Erasing metadata for release $1"
    __kill_release "$1"
}

# Given a release, show any redundant barclamp declarations.
# This is intended to help manually clean up release metadata.
show_duplicate_barclamps() {
    local release=${1:-$(current_release)} build parent bc
    local to_remove=()
    for build in $(builds_in_release "$release"); do
        build="$release/$build"
        parent=$(parent_build "$build")
        [[ $parent ]] || continue
        local -A barclamps
        for bc in $(barclamps_in_build "$parent"); do
            barclamps["$bc"]=parent
        done
        for bc in $(barclamps_from_build "$build"); do
            [[ ${barclamps[$bc]} ]] || continue
            [[ $(get_barclamp_branch_for_build "$build" "$bc") = \
                $(get_barclamp_branch_for_build "$parent" "$bc") ]] || continue
            to_remove+=("$build: $bc")
        done
    done
    if [[ $to_remove ]]; then
        debug "Release $release has the following redundant barclamp metadata:"
        printf "%s\n" "${to_remove[@]}" |sort -u
    else
        debug "No redundant barclamps in release $release"
    fi
}

# Back up any local commits that are not already present on our upstreams,
# or that have not already been backed up.
backup_repo() {
    local id bc branch remote branch_get_func branch_backup_func
    local -A branches backup_remotes
    local backup_method target_remote target_method target_prefix
    local remote_re='^refs/remotes/([^/]+)'
    while read id branch; do
        if [[ $branch = refs/heads/* ]]; then
            # This is a local ref, see if it needs backed up.
            branch=${branch#refs/heads/}
            # Does this branch have a remote, and is it one that Crowbar cares about?
            remote=$(remote_for_branch "$branch") || continue
            crowbar_remote_exists "$remote" && remote_available "$remote" || continue
            # If we already know what the backup remote is for this branch is based
            # on the upstream remote for the branch is, carry on.
            [[ ${backup_remotes[$remote]} ]] || {
                # Otherwise, firgure out whether this branch is from a remote we are backing up.
                backup_remote=$(get_repo_cfg "crowbar.backup.$remote.remote") && \
                    crowbar_remote_exists "$backup_remote" && \
                    git_remote_exists "$backup_remote" && \
                    remote_available "$backup_remote" || \
                    continue
                backup_remotes[$remote]="${backup_remote}"
            }
            if ! git rev-parse --verify -q \
                "refs/remotes/${backup_remotes[$remote]}/$branch" &> /dev/null || \
                [[ $(git rev-parse "refs/remotes/${backup_remotes[$remote]}/$branch") != \
                $(git rev-parse "refs/heads/${branch}") ]]; then
                # Only back up branches that either don't exist on the backup remote
                # for this branch, or that do exist but don't point at the commit we want.
                branches[${backup_remotes[$remote]}]+="$branch "
            fi
        elif [[ $branch =~ $remote_re ]]; then
            # This is a remote ref, see if we care about it and need to delete it.
            remote="${BASH_REMATCH[1]}"
            # Is this a remote we are using as a backup target?
            is_in "$remote" "${backup_remotes[*]}" || continue
            branch=${branch#refs/remotes/${remote}/}
            # Skip any pull request branches.
            [[ $branch = pull-req-* || $branch = HEAD ]] && continue
            # Skip any branches that have a local head.
            git show-ref --verify --quiet "refs/heads/$branch" && continue
            # Schedule the branch for deletion.
            branches["$remote"]+=":${branch} "
        fi
    done < <(LC_ALL=C git show-ref |sort -k2) # Sort ensures that local refs always come first.
    # Now, we know what to back up, what to ignore, and what to delete.
    # Make it so.
    for remote in "${!branches[@]}"; do
        [[ ${branches[$remote]} ]] || continue
        git_push -f "$remote" ${branches[$remote]}
    done
}

# Back up everything to your persoal remote.
backup_everything() {
    local bc remote branches=() branch remotes=()
    local -A touched_bcs
    crowbar_remote_exists personal && remote_is_github personal || \
        die "You must have a remote named personal to back things up." \
        "Try running dev setup or dev remote add personal <personal repo urlbase>"
    # Back up all the barclamps that are references as submodules for
    # branches that this remote is "authoritative" for.
    for bc in "$CROWBAR_DIR/barclamps/"*; do
        [[ -d $bc/.git || -f $bc/.git ]] || continue
        debug "Backing up barclamp ${bc##*/}"
        in_barclamp "${bc##*/}" backup_repo
    done
    debug "Backing up Crowbar"
    in_repo backup_repo
}

# Misnamed, this can pin a release, specific build,
# or a barclamp in a specific build.
# pin_release and unpin_release use this function to do all their work.
__pin_release() {
    [[ $1 ]] || die "Must pass a release, release/build, or release/build/barclamp to pin."
    local bc build ref
    if barclamp_exists_in_build "$1"; then
        build=${1%/*} bc=${1##*/}
        ref="${2:-$(in_barclamp "$bc" git rev-parse HEAD)}"
        barclamp_branch_for_build "$build" "$bc" "$ref" || return 1
    elif build_exists "$1"; then
        for bc in $(barclamps_in_build "$1"); do
            __pin_release "$1/$bc" "$2" || return 1
        done
    elif release_exists "$1"; then
        for build in $(builds_in_release "$1"); do
            __pin_release "$1/$build" "$2" || return 1
        done
    else
        echo "Don't know what $1 is!" >&2
        return 1
    fi
}

# Pin barclamps in a release to a specific tag or to their current HEAD.
pin_release() {
    barclamps_are_clean || die "Crowbar must be clean before pinning $1"
    if __pin_release "$@"; then
        __release_update "Pinned barclamps in $1 to ${2:-current HEAD}"
        switch_release
        return 0
    else
        __release_cleanup
        echo "Could not pin $1 to ${2:-current HEAD}, leaving things unchanged."
        return 1
    fi
}

# Reset barclamps in a release back to tracking the correct release branch.
unpin_release() {
    [[ $1 ]] || die "Must pass a release, release/build, or release/build/barclamp to unpin."
    local bc build
    if barclamp_exists_in_build "$1"; then
        build=${1%/*}
    elif build_exists "$1"; then
        build="$1"
    elif release_exists "$1"; then
        build="$1/master"
    else
        die "Don't know what $1 is!"
    fi
    pin_release "$1" "$(build_branch "$build")"
}


# Create a new tagged release based on the current state of the repositories.
cut_tagged_release() {
    # $1 = name of the tag.
    # $2 = build to cut from.
    # The build we are cutting from would have already been pinned to the tag.
    release_exists "$1" && die "Cannot cut tagged release $1, it already exists!"
    build_exists "$2" || die "Cannot cut tagged release, basis build $2 does not exist!"
    local bc ref f
    # Start with a clone of the release.
    clone_release "${2%/*}" "$1"
    # Add tagged refs for all the barclamps we care about to
    # the master build in the new release
    for bc in $(barclamps_in_build "$2"); do
        ref=$(barclamp_branch_for_build "$2" "$bc")
        [[ $ref && $ref != empty-branch ]] || continue
        # We assume that the barclamp tags have already been created.
        __add_barclamp_to_build "$1/master" "$bc" "$1"
    done
    # Make sure we copy down build-specific non-barclamp metadata
    (
        cd "$(build_cfg_dir "$1/${2##*/}")"
        target_dir="$(build_cfg_dir "$1/master")"
        for f in *; do
            [[ $f = barclamp-* ]] && continue
            [[ $f = . || $f = .. || $f = parent ]] && continue
            cp -aL "$f" "$target_dir"
        done
        (cd "$target_dir"; git add .)
    )
    # Exterminate all now-unneeded non-master builds
    local build
    while read build; do
        [[ $build = master ]] && continue
        __kill_build "$1/$build"
    done < <(__builds_in_release "$1")
    # Create a new fork in the build cache.
    if git_managed_cache; then
        local old_rel_br=$(release_branch "${2%/*}")
        local rel_br="$(release_branch "$1")"
        in_cache branch_exists "$old_rel_br" && \
            in_cache git branch "$rel_br" "$old_rel_br"
    fi
    __release_update "Cut tagged release $1"
    switch_release "$1"
}


# Find branches in barclamps for a given release that are not synced,
# and show them.
find_unsynced_branches_for_release() {
    # $1 = release
    local bc rel line changes_found=false sha ref br build
    local barclamps=() builds=()
    rel="${1:-$(current_release)}"
    if release_exists "$rel"; then
        barclamps=($(barclamps_in_release "$rel"))
        builds=($(builds_in_release "$rel"))
    elif build_exists "$rel"; then
        barclamps=($(barclamps_in_build "$rel"))
        builds=("$rel")
    else
        die "Release $rel does not exist, cannot find unsynced changes for it!"
    fi
    for bc in "${barclamps[@]}"; do
        local branches=()
        local from to br
        for build in "${builds[@]}"; do
            __barclamp_exists_in_build "$rel/$build/$bc" || continue
            ref=$(barclamp_branch_for_build "$rel/$build" "$bc")
            [[ $ref = empty-branch ]] && \
                die "Barclamp $bc should exist for $rel/$build, but it is set to empty-branch!"
            break
        done
        upstream="$(in_barclamp "$bc" remote_for_branch "$ref")" || continue
        case $changes_from in
            remote) from="refs/heads/$ref" to="refs/remotes/$upstream/$ref";;
            local)  from="refs/remotes/$upstream/$ref" to="refs/heads/$ref";;
            *) die "Cannot happen in find_unsynced_branches_for_release"
        esac
        in_barclamp "$bc" branches_synced '.' "$from" "$to" && continue
        changes_found=true
        printf "%s: %s -> %s\n" "$bc" "${from#refs/*/}" "${to#refs/*/}"
        in_barclamp "$bc" git --no-pager log --oneline "${from#refs/*/}..${to#refs/*/}"
        echo
    done
    if [[ $changes_found = false ]]; then
        debug "No unsynced changes for release $rel"
        return 1
    fi
    return 0
}

find_local_changed_branches_for_release() {
    local changes_from="local"
    find_unsynced_branches_for_release "$@"
}

find_remote_changed_branches_for_release() {
    local changes_from="remote"
    find_unsynced_branches_for_release "$@"
}

# Merge (or rebase) changes into the specificed branch from the corresponding
# branch on the specified remote.
merge_or_rebase_from_remote() {
    # $1 = remote
    # $2 = local branch
    local remote branch rebase_temp
    remote="$1" branch="$2" merge_temp="$branch-$btemp"
    git rev-parse --verify -q "$remote/$branch" &>/dev/null || return 0
    branches_synced "." "refs/heads/$branch" "refs/remotes/$remote/$branch" && return 0
    quiet_checkout "$branch" || return 1
    git branch -f --no-track "$merge_temp" "$branch"
    if crowbar_remote_exists "$remote" && \
        [[ ! $DEV_FROM_REMOTES ]]; then
        if git rebase -p -q "$remote/$branch" "$branch" &>/dev/null; then
            debug "  Rebased $branch onto $remote/$branch"
            return 0
        else
            git rebase --abort
            git reset --hard "$merge_temp"
            debug "  Rebase failed, will try merge."
        fi
    fi
    if git merge -q "$remote/$branch"; then
        debug "  Merged $remote/$branch merged into $branch"
        return 0
    fi
    git merge --abort
    git reset --hard "$merge_temp"
    git branch -D "$merge_temp"
    debug "  Merge conflicts merging $remote/$branch into $branch"
    return 1
}

merge_one_release() (
    [[ $1 && $2 && $3 ]] || die "Not enough params!"
    cd "$CROWBAR_DIR/barclamps/$1"
    local t s
    s="$(barclamp_branch_for_build "$2" "$1")"
    t="$(barclamp_branch_for_build "$3" "$1")"
    [[ $s && $t && $s != empty-branch && $t != empty_branch ]] || return 0
    quiet_checkout "$s" || return 1
    debug "Merging $3 into $2 in barclamp $1"
    git merge -q "$t" && return 0
    echo "Barclamp ${1}:"
    echo "  Merging branch $t into $s failed."
    echo "  Dropping to a shell for you to fix up."
    echo "  If you want to abort this merge, exit the shell with 'exit 1'"
    /bin/bash && return 0
    git merge --abort
    return 2
)

# Handle merges across releases.
merge_releases() {
    # $@ = releases to merge into the current release
    local rel branch head build bc thisrel res
    local -A current_barclamps current_builds
    thisrel=$(current_release)
    bc="${repo##*/}"
    current_release_br=$()
    for bc in $(barclamps_in_release "$thisrel"); do
        current_barclamps[$bc]="$thisrel"
    done
    for build in $(builds_in_release "$thisrel"); do
        current_builds[$build]="$thisrel"
    done
    for rel in "$@"; do
        [[ $rel && $rel != $thisrel ]] && release_exists "$rel" || continue
        for build in $(builds_in_release "$rel"); do
            [[ ${current_builds[$build]} ]] || continue
            for bc in $(barclamps_from_build "$rel/$build"); do
                [[ ${current_barclamps[$bc]} ]] || continue
                head=$(get_current_head)
                merge_one_release "$bc" "$thisrel/$build" "$rel/$build"
                res=$?
                in_barclamp "$bc" git checkout -q -f "${head}"
                case $res in
                    1) debug "No branch for $thisrel/$build in $bc to merge into. Skipping.";;
                    2) debug "Barclamp ${repo##*/}: Merge of $br from $rel into $thisrel failed."
                        return 1;;
                esac
            done
        done
    done
    debug "Releases $* merged into $thisrel"
    debug "You will need to update the metadata manually."
}

# Merges in changes into all local branches from their upstreams.
# Assumes that upstream commits have already been fetched from the proper
# remotes by running dev fetch.
sync_repo() (
    local branch head b rel bc remote ref repo="$1" res=0
    shift
    # $repo = dir to CD to initially.
    # Assumes that remote has already been fetched.
    cd "$repo"
    # Repo is not clean, we will refuse to merge in any case.
    git_is_clean || exit 1
    # Merge upstream branches from our remotes
    head=$(get_current_head)
    while read ref branch; do
        branch=${branch#refs/heads/}
        remote="$(remote_for_branch "$branch")" || continue
        merge_or_rebase_from_remote "$remote" "$branch" && continue
        res=1
    done < <(git show-ref --heads)
    quiet_checkout "${head#refs/heads/}"
    if [[ $res = 1 ]]; then
        debug "Merge conflicts detected when syncing barclamp ${repo##*/} with $remote."
        debug "Please fix them up locally and retry the sync operation."
        return 1
    fi
    return 0
)

# Either unwind any merges/rebases performed as part of a sync or
# commit them.
unwind_or_commit_barclamp_syncs() {
    # $1 = "unwind" or "commit"
    local bc
    for bc in "$CROWBAR_DIR/barclamps/"*; do
        (
            cd "$bc"
            while read sha ref; do
                [[ $ref = *-$btemp ]] || continue
                ref=${ref#refs/heads/}
                if [[ $1 = unwind ]]; then
                    debug "${bc##*/}: Unwinding last sync of ${ref%-$btemp}"
                    git branch -f "${ref%-$btemp}" "$ref"
                fi
                git branch -D "$ref"
            done < <(git show-ref --heads)
        )
    done
}

# Merge all changes from our upstreams for all barclamps and the main Crowbar
# repository.
sync_everything() {
    local unsynced_barclamps=()
    local b u head res=0 ref branch rel
    local btemp="$$-${RANDOM}"
    barclamps_are_clean || exit 1
    # Do barclamps first.
    for b in "$CROWBAR_DIR/barclamps/"*; do
        debug "Syncing barclamp ${b##*/}:"
        sync_repo "$CROWBAR_DIR/barclamps/${b##*/}" "$@"
        case $? in
            1) unsynced_barclamps+=("${b##*/}");;
            2)
                debug "Merging releases aborted."
                debug "Undoing any merges that succeeded:"
                unwind_or_commit_barclamp_syncs unwind
                return 1;;
        esac
    done
    if [[ $unsynced_barclamps ]]; then
        echo "Unable to sync:" >&2
        for b in "${unsynced_barclamps[@]}"; do
            printf "  %s\n" "$b"
        done
        echo "Unwinding syncs that did succeed:"
        unwind_or_commit_barclamp_syncs unwind
        echo "Please fix things up and rerun sync."
        return 1
    fi
    unwind_or_commit_barclamp_syncs commit
    # Finished with barclamps, now for crowbar.
    if in_repo git_is_clean; then
        debug "Syncing crowbar"
        if in_repo merge_or_rebase_from_remote "$(remote_for_branch master)" master; then
            in_repo branch_exists "master-$btemp" && in_repo git branch -D "master-$btemp"
            return 0
        fi
        echo "Please fix things up and do the merge manually."
        return 1
    else
        debug "Main Crowbar is not clean, not updating it."
    fi
}

dev_short_help() {
    local cmd
    echo "Available commands:"
    echo
    for cmd in $(for cmd in "${!DEV_SHORT_HELP[@]}"; do echo "$cmd"; done |sort); do
        echo "${cmd}: ${DEV_SHORT_HELP[$cmd]}"
    done
    echo
    echo 'For detailed help in a specific command, run dev help <command>.'
    echo 'README.dev-and-workflow has a general overview of the dev tool.'
    echo 'To see all the help at once, run dev help all'
}


dev_bash_completion() {
  cmds=$(printf "%s" "${!DEV_COMMANDS[*]}"; )
cat > ~/.bash_completion <<EOF
_dev()
{
  local cur prev opts
  COMPREPLY=()
  cur="\${COMP_WORDS[COMP_CWORD]}"
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
  opts="${cmds}"

  #opts="--help --verbose --version"

  #if [[ \${cur} == -* ]] ; then
  COMPREPLY=( \$(compgen -W "\${opts}" -- \${cur}) )
  return 0
  #fi
}
complete -F _dev dev
EOF


}


dev_help () {
    (
    echo "$0: Development helper for Crowbar"
    echo
    if [[ $1 ]]; then
        if [[ ${DEV_LONG_HELP[$1]} ]]; then
            echo "$1: ${DEV_LONG_HELP[$1]}"
        elif [[ $1 = all ]]; then
            for cmd in $(for cmd in "${!DEV_LONG_HELP[@]}"; do echo "$cmd"; done |sort); do
                echo "${cmd}: ${DEV_LONG_HELP[$cmd]}"
                echo
            done
        else
            echo "$1: No help for $1"
        fi
    else
        dev_short_help
    fi
    ) |less
}

# Tests to see if the given branch in a repo needs a pull request.
branch_needs_pull_req() {
    # $1 = local branch
    # $2 = (optional) target branch
    local target="${2:-$1}"
    local upstream="$to_remote/$target"
    git rev-parse --verify -q "$upstream" &>/dev/null || return 1
    branches_synced '.' "refs/remotes/$upstream" \
        "refs/heads/$1" && return 1
    return 0
}

# Push local updates for a release out to its "best" upstream, or
# whatever DEV_TO_REMOTES says to push to.
# This function tries to ensure that it will fail without doing anything
# if there will be any problems (due to authetication or whatever).
push_release() {
    local rel_br release remotes remote bc res cmd build
    local pushcmds=() to_remotes=()
    local -A branches
    release="$(current_release)"
    if [[ $1 ]]; then
        release_exists "$1" || \
            die "Cannot push non-existent local release $1"
        release="$1"
    fi
    local can_push=true
    for bc in $(barclamps_in_release "$release"); do
        for rel_br in $(barclamp_branches_for_release "$release" "$bc"); do
            [[ $DEV_TO_REMOTES ]] && to_remotes=("${DEV_TO_REMOTES[@]}") || \
                to_remotes=("$(in_barclamp "$bc" remote_for_branch "$rel_br")")
            [[ $to_remotes ]] || to_remotes=("$(in_barclamp "$bc" origin_remote)")
            for remote in "${to_remotes[@]}"; do
                remote_available "$remote" || continue
                if ! probe_barclamp_remote "$bc" "${DEV_REMOTE_URLBASE[$remote]}" &>/dev/null; then
                    debug "Barclamp $bc does not exist at $remote, skipping."
                    continue
                fi
                if ! res="$(in_barclamp "$bc" git push -nq "$remote" -- "${rel_br}:${rel_br}")"; then
                    debug "$bc: $rel_br -> $remote will fail."
                    echo "$res" >&2
                    exit 1
                fi
                debug "$bc: $rel_br -> $remote is OK."
                pushcmds+=("in_barclamp $bc git_push $remote -- ${rel_br}:${rel_br}")
            done
        done
    done
    debug "Test passed, pushing branches."
    for cmd in "${pushcmds[@]}"; do
        if [[ $DRY_RUN = true ]]; then
            echo "Would have run: $cmd"
        else
            $cmd || die "Could not push $rel_br to $remote after test push passed."
        fi
    done
    debug "If you just pushed a new release, be sure and push the metadata as well."
}

# Make sure everything is up to date, and then figure out what
# barclamps and branches might need pull requests on Github.
# Once we have that figured out, print out a command line that can
# be used by dev pull-requests-gen to actually generate the pull requests.
pull_requests_prep() {
    remote_available personal && remote_is_github personal || \
        die "No personal remote available at Github.  Cannot do pull requests."
    barclamps_are_clean || exit 1
    local skip_sync merge
    while (( $# > 0)); do
        case $1 in
            --skip-sync)
                skip_sync=true
                shift;;
            --merge)
                merge=true
                shift;;
        esac
    done

    if ! [[ $skip_sync ]]; then
        fetch_all && sync_everything || \
            die "Unable to synchronize remotes for pull requests"
    fi
    local barclamps_to_push=()
    local to_remote="$(origin_remote)"
    [[ $DEV_TO_REMOTES ]] && to_remote="$DEV_TO_REMOTES"
    remote_is_github "$to_remote" || \
        die "Cannot issue pull requests for $to_remote, it is not from github."
    local branch bc build release=$(current_release)
    local target_release="$release"
    if [[ $merge ]]; then
        target_release="$(find_best_parent "$release")" || \
            die "No parent release to merge into!"
        echo "Will generate pull requests for merge $release into $target_release" >&2
    fi
    for bc in $(barclamps_in_release $release); do
        for branch in $(barclamp_branches_for_release "$release" "$bc"); do
            local target_branch=$(barclamp_branches_for_release "$target_release" "$bc")
            in_barclamp "$bc" branch_needs_pull_req "$branch" "$target_branch" || continue
            barclamps_to_push+=("$bc")
            break
        done
    done
    [[ ${push_master} || ${barclamps_to_push} ]] || {
        echo "Everything up to date, no pull requests are possible."
        return 0
    }
    echo "Barclamps that might need pull requests: ${barclamps_to_push[*]-(none)}"
    [[ ${push_master} ]] && echo "Crowbar needs push"
    echo "Command to generate pull requests:"
    echo -n "$0 pull-requests-gen --to $to_remote --release $release"
    [[ $target_release != $release ]] && \
        echo -n " --merge"
    [[ ${push_master} ]] && \
        echo -n " --branches master"
    [[ ${barclamps_to_push[*]} ]] && \
        echo -n " --barclamps ${barclamps_to_push[*]}"
    echo
}

# Actaully generate a pull request by using make_pull_request to
# create the JSON blob that github needs, and then posting that to the
# right URL at Github.
do_pull_request() {
    # $1 = url to POST to
    # rest of args passed verbatim to make_pull_request helper.
    local posturl="$1" lines
    local -A res
    shift
    if [[ $DEBUG || $DRY_RUN ]]; then
        make_pull_request "$@"
        return
    fi
    lines="$( make_pull_request "$@" | \
        curl_and_res -X POST --data @- \
        -H "Content-Type: application/vnd.github.beta.raw+json" \
        "$posturl")" || die "Error communicating with Github." \
        "Please delete any pull requests that succeeded and try again."
    . <(printf '%s' "$lines" | parse_yml_or_json - res) || {
        die "Error parsing response from Github." \
            "Response was:" \
            "$lines"
            "Please delete the pull requests that succeeded and try again."
    }
    if [[ ${res['number']} && ${res['html_url']} ]]; then
        printf "Pull request %s created (%s)\n" \
            "${res['number']}" "${res['html_url']}"
    else
        die "Pull request to $posturl with following params failed:" \
            "$@" \
            "Response was:" \
            "$lines" \
            "Please delete the pull requests that succeeded and try again."
    fi
}

# Get the diffstat from the origin branch of the branch passed,
# or print an error message if there is no origin.
diffstat_from_upstream() {
    local upstream=''
    upstream="$to_remote/$1"
    if git rev-parse --verify -q "$upstream" &>/dev/null; then
        git diff --stat "$upstream" "$1"
    else
        echo "No origin to generate diffstat"
    fi
}

# Simple helper for printing a short probably unique name for a branch.
git_ref() {
    # $1 = branch
    printf "%s-%s" "${1//\//-}" "$2"
}

# Make pull requests based on the command line args passed.
# These should follow the command line arguments that
# pull_requests_prep generated.
pull_requests_gen() {
    # $@ = options to parse
    dev_is_setup || die "You must run dev setup before trying to generate pull requests."
    remote_available personal && remote_is_github personal || \
        die "No personal remote available at Github.  Cannot do pull requests."
    local -A barclamps branches barclamp_branches bc_pulls br_pulls refs
    local bc br bcr bcb title body option bc_name head release target_release
    local merge_into_parent=false
    local prc=0 n=1
    local to_remote="$(origin_remote)"
    [[ $DEV_TO_REMOTES ]] && to_remote="$DEV_TO_REMOTES"
    local to_account="$(remote_github_account "$to_remote")" || \
        die "Cannot issue pull requests for $to_remote, it is not from github."
    # This is needed to make sure we can see parse_yml_or_json.
    # It should go away at some point.
    # Parse our options and validate them.
    while [[ "$1" ]]; do
        case $1 in
            --branches)
                shift
                while [[ $1 && $1 != '--'* ]]; do
                    br="$1"
                    shift
                    in_repo branch_exists "$br" || \
                        die "$br is not a branch in Crowbar!"
                    in_repo git_remote_exists "$to_remote" || \
                        die "Cannot make pull request to $to_remote, we don't know about it."
                    br_pulls["$br"]="true"
                done;;
            --barclamps)
                shift
                while [[ $1 && $1 != '--'* ]]; do
                    bc="${1%%/*}"
                    br="${1#*/}"
                    [[ $bc = $br ]] && br="calculate"
                    barclamps["$bc"]="true"
                    barclamp_branches["$bc"]+=" $br"
                    shift
                done;;
            --release)
                shift
                release_exists "$1" || die "$1 is not a release"
                release="$1"
                shift;;
            --merge)
                shift
                merge_into_parent=true;;
            *)
                die "Unknown option $1 to $0 pull-requests-gen!";;
        esac
    done

    [[ $release ]] || release=$(current_release)
    target_release=$release
    if [[ $merge_into_parent = true ]]; then
        target_release=$(find_best_parent "$release") || \
            die "No parent release to merge into!"
    fi

    switch_release "$release"
    # OK, sanity-check any barclamp branches we were passed.
    for bc in "${!barclamps[@]}"; do
        bcb=''
        for br in ${barclamp_branches[$bc]}; do
            [[ $br = calculate ]] && br=$(barclamp_branches_for_release "$release" "$bc")
            in_barclamp "$bc" branch_exists "$br" || \
                die "$br is not a branch in barclamp $bc!"
            in_barclamp "$bc" git_remote_exists "$to_remote" || \
                die "Cannot make pull request from barclamp $bc -- it is not present at remote $to_remote!"
            is_in "$br" "$bcb" || bcb+=" $br"
        done
        barclamp_branches[$bc]="$bcb"
    done

    # Generate a very probably unique name for the pull request.
    local pull_id="$(
        (
            for bc in "${!barclamps[@]}"; do
                for bcb in ${barclamp_branches["$bc"]}; do
                    in_barclamp "$bc" git show-ref --hash "refs/heads/$bcb"
                done
            done
            for br in "${!br_pulls[@]}"; do
                in_repo git show-ref --hash "refs/heads/$br"
            done
            hostname -f
            date -u '+%s.%N'
        )|sha1sum |cut -d ' ' -f 1)"
    for bc in "${!barclamps[@]}"; do
        for bcb in ${barclamp_branches["$bc"]}; do
            bc_pulls["$bc/$bcb"]="pull-req-$(in_barclamp "$bc" git_ref "$bcb" "$pull_id")"
        done
    done
    # OK, now we know how many pull requests we have to issue.
    prc=$((${#bc_pulls[@]} + ${#br_pulls[@]}))

    # Get the common title and body for the pull request.
    body="$(mktemp /tmp/crowbar-pull-req-XXXXXXXX)"
    if [[ $DEBUG || $DRY_RUN ]]; then
        title="Test"
        echo "test" >> "$body"
    else
        echo "Enter a title for this pull request series."
        echo "After you have entered a title, an editor will open, and you can"
        echo "enter anything you want for the body of the pull requests."
        read -p "Title: " title
        if [[ $EDITOR ]]; then
            $EDITOR "$body"
        else
            nano "$body"
        fi
    fi

    # issue the pull requests for our barclamps.
    for barclamp in "${!bc_pulls[@]}"; do
        local bc=${barclamp%%/*}
        local bc_base=${barclamp#*/}
        in_barclamp "$bc" git_push personal "$bc_base:${bc_pulls[$barclamp]}"
        local bc_target="$(barclamp_branches_for_release "$target_release" "$bc")"
        local bc_head="$DEV_GITHUB_ID:${bc_pulls[$barclamp]}"
        local bc_name=$(in_barclamp $bc git_url_for_remote $to_remote)
        bc_name=${bc_name##*/}
        bc_name=${bc_name##*:}
        bc_name=${bc_name%.git}
        do_pull_request \
            "https://api.github.com/repos/$to_account/$bc_name/pulls" \
            --title "$title [$n/$prc]" --base "$bc_target" --head "$bc_head" \
            --body "@$body" \
            --body "$(in_barclamp "$bc" diffstat_from_upstream "$bc_target")" \
            --body "Crowbar-Pull-ID: $pull_id" \
            --body "Crowbar-Release: $release"
        n=$(($n + 1))
    done

    # Now, issue the requests for branches.
    # Make sure they are ordered correctly.
    for br in "${!br_pulls[@]}"; do
        local br_pull_name="pull-req-$(in_repo git_ref "$br" "$pull_id")"
        in_repo git_push personal "${br}:${br_pull_name}"
        local br_head="$DEV_GITHUB_ID:${br_pull_name}"
        do_pull_request \
            "https://api.github.com/repos/$to_account/crowbar/pulls" \
            --title "$title [$n/$prc]" --base "$br" --head "$br_head" \
            --body "@$body" --body "$(in_repo diffstat_from_upstream "$br")" \
            --body "Crowbar-Pull-ID: $pull_id" \
            --body "Crowbar-Release: $release"
        n=$(($n + 1))
    done
    rm -f "$body"
}

# Unconditionally push either the current branch or the branches
# passed on the command line to the personal remote.
push_branches() {
    # $@ = Local branches to push
    dev_is_setup || die "You must run dev setup before pushing branches to Github."
    remote_available personal && remote_is_github personal || \
        die "No personal remote available at Github.  Cannot do pull requests."
    local branches=("$@") br btp=()
    [[ $branches ]] || br=($(in_repo git symbolic-ref HEAD)) || \
        die "Main Crowbar repo is not on a branch we can push!"
    [[ $br ]] && branches=("${br#refs/heads/}")
    for br in "${branches[@]}"; do
        if in_repo git rev-parse --verify -q "$br" &>/dev/null; then
            btp+=("$br")
        else
            echo "$br is not a branch I can push!"
        fi
    done
    echo "Pushing ${btp[*]} to your Github fork of Crowbar."
    in_repo git_push personal "${btp[@]}"
}

# Show the releases that either the local repo or the origin repo knows about.
show_releases() { all_releases; }

# Print the proper name of a branch in a barclamp for a release.
release_branch() { build_branch "${1:-$(current_release)}/master"; }

barclamp_branches_for_release() {
    # $1 = release
    # $2 = barclamp
    local -A branches
    local build br
    release_exists "$1" || die "$1 is not a release!"
    for build in $(builds_for_barclamp_in_release "$2" "$1"); do
        br=$(barclamp_branch_for_build "$1/$build" "$2")
        [[ $br && $br != empty-branch ]] || continue
        branches[$br]="$br"
    done
    for br in "${branches[@]}"; do
        echo "$br"
    done
}

refs_for_build() {
    local bc br ref build
    build="${1:-$(current_build)}"
    build_exists "$build" || die "$build is not a build!"
    echo "build: $build"
    echo "crowbar: $(in_repo git rev-parse -q HEAD)"
    for bc in $(barclamps_in_build "$build"); do
        br=$(barclamp_branch_for_build "$build" "$bc")
        [[ $br = empty-branch ]] && continue
        ref=$(in_barclamp "$bc" git rev-parse -q "$br") || continue
        echo "barclamps/$bc: $ref"
    done
    if [[ -d $CACHE_DIR/.git ]] && br="$(build_branch "$build")" && in_cache branch_exists "$br"; then
        echo "build-cache: $(in_cache git rev-parse -q "$br")"
    fi
}

new_feature() {
    [[ $1 ]] || die "Please give your new feature a name!"
    [[ $2 ]] && die "new-feature only takes one parameter."

    cut_release "feature/$1"
}

maybe_checkout_build_cache_branch() {
    # $1 = release
    # This is here so that we only switch branches in the build cache if
    # we are building a release, because it can be too time-consuming otherwise.
    git_managed_cache || return 0
    local rel_br
    rel_br="$(release_branch "${1:-$(current_release)}")" || \
        die "Cannot find release branch for $1"
    update_cache_tracking_branches
    if in_cache git show-ref | grep -q "$rel_br"; then
        debug "Checking out $rel_br in the build cache, please be patient."
        in_cache quiet_checkout -f "$rel_br"
    fi
}

# With no args, echo the current build being worked.
# Otherwise, build a Crowbar ISO after sanity-checking the current env.
build_crowbar() {
    local build_args=() b c target_os target_build target_release target_branch exact
    local switch_include_cache=true no_switch=false
    target_release=$(current_release)
    target_branch=$(current_build)
    target_branch=${target_branch##*/}
    [[ $1 ]] || { current_build; return 0; }
    barclamps_are_clean || exit 1
    while [[ $1 ]]; do
        case $1 in
            --os) shift; target_os="$1";;
            --release) shift; target_release="$1";;
            --branch) shift; target_branch="$1";;
            --exact) exact=true;;
        --no-switch) no_switch=true;;
        --no-cache-update) switch_include_cache=false;;
            *) build_args+=($1);;
        esac
        shift
    done
    [[ $target_os ]] || \
        die "Cannot build Crowbar, need to know what OS to stage it on!"
    build_args+=("--skip-lock")
    if [[ $no_switch = false ]]; then
        switch_release "${target_release}" || \
            die "Could not switch to $target_release for build!"
        checkout "$target_branch"
    fi
    if [[ $exact ]]; then
        debug "Exact does not mean anything anymore."
    fi
    if [[ $switch_include_cache = true ]];then
    maybe_checkout_build_cache_branch
    else
    debug "--no-cache-update enabled. So skip checking out the cache"
    fi
    with_build_lock exec "$CROWBAR_DIR/build_crowbar.sh" \
        "$target_os" "${build_args[@]}"
    debug "Build process took ${SECONDS} seconds."
}

# Reset a branch to another ref.
reset_branch() {
    # $1 = branch to reset
    # $2 = target to reset it to.
    if [[ $(git symbolic-ref HEAD) = refs/heads/$1 ]]; then
        git reset --hard "$2"
    else
        git branch -f --no-track "$1" "$2"
    fi
}

# Reset a release to either the last backed up version
# or the current upstream version.
reset_release() {
    barclamps_are_clean || exit 1
    local target_release=$(current_release) target=backup build
    local -A barclamp_targets
    while [[ $1 ]]; do
        case $1 in
            --release) shift; target_release="$1";;
            --target) shift; target="$1";;
            *) die "Unknown option $1 passed to reset-release";;
        esac
        shift
    done
    release_exists "$target_release" || \
        die "Release $target_release does not exist, cannot reset."
    if [[ $target = backup ]]; then
        remote_available personal && remote_is_github personal || \
            die "No personal remote available at Github.  Cannot restore from backup."
    fi
    for bc in $(barclamps_in_release "$target_release"); do
        [[ -d $CROWBAR_DIR/barclamps/$bc/.git || \
            -f $CROWBAR_DIR/barclamps/$bc/.git ]] || continue
        for target_branch in $(barclamp_branches_for_release "$target_release" "$bc"); do
            case $target in
                upstream) barclamp_targets[$bc:$target_branch]="$(in_barclamp "$bc" remote_for_branch "$target_branch")/$target_branch" || \
                    die "No tracking branch for $br in $bc, cannot restore from backup.";;
                backup) barclamp_targets[$bc:$target_branch]+="personal/$target_branch";;
                *) die "Don't know how to reset barclamp $bc to $target_method";;
            esac
            if ! in_barclamp "$bc" git rev-parse --quiet --verify \
                "${barclamp_targets[$bc:$target_branch]}" &>/dev/null; then
                if [[ $target = backup ]]; then
                    debug "${barclamps[$bc:$target_branch]} in $bc has never been backed up, skipping."
                    unset barclamp_targets[$bc:$target_branch]
                elif [[ $target = upstream ]]; then
                    debug "Cannot reset barclamp $bc to ${barclamp_targets[$bc]}"
                    unset barclamp_targets[$bc:$target_branch]
                fi
            fi
        done
        for bc in "${!barclamp_targets[@]}"; do
            in_barclamp "${bc%%:*}" \
                reset_branch "${bc#*:}" "${barclamp_targets[$bc]}"
        done
    done
}

# Migrate Crowbar from using branches and submodules to using a flattened
# tree. flatten_crowbar flattens everything -- you may need to clean up.
flatten_crowbar() {
    local r b bc br rel rel_parent rel_dir gmode gtype gsha gpath build
    local br_re='^refs/(heads|remotes/[^/]+)/(.*)$'
    local bc_url_re='^submodule\.(.+)\.url=.+[:/]([^/:]+)$'
    local -A remotes
    if [[ ! $1 ]]; then
        remotes[$(origin_remote)]=true
    else
        for r in "$@"; do
            crowbar_remote_exists "$r" || \
                die "Cannot flatten repo information for remote $r"
            remotes[$r]=true
        done
    fi
    crowbar_is_clean && \
        [[ $(in_repo git symbolic-ref HEAD) = refs/heads/master ]] || \
        die "You must be on master to flatten the hierarchy!"
    git_tracked_checkout && die "Cannot flatten Git tracked metadata"
    flat_checkout && die "Crowbar is already flattened."
    while read sha ref; do
        # Special case handling for tags.
        # We make releases for them, and have their barclamp markers point to
        # exact commits.
        if [[ $ref = refs/tags/* && ! ( $ref =~ jenkins ) ]]; then
            br="${ref#refs/tags/}"
            build="${br}/master"
        elif [[ $ref =~ $br_re ]]; then
            [[ ${BASH_REMATCH[1]} = remotes/* && \
                ! ${remotes[${BASH_REMATCH[1]#remotes/}]} ]] && \
                continue
            br="${BASH_REMATCH[2]}"
            [[ ${DEV_BRANCHES[${br##*/}]} ]] || continue
            if [[ $br = ${br##*/} ]]; then
                build="development/$br"
            elif [[ $br = release/* ]]; then
                build="${br#release/}"
            elif [[ $br = feature/* ]]; then
                build=$br
            else
                continue
            fi
        else
            continue
        fi
        build_exists "$build" && continue
        rel=${build%/*}
        build_dir="$CROWBAR_DIR/.releases/$build"
        [[ -d $build_dir ]] && continue
        # If the ref we are looking at has already been flattened,skip it.
        [[ $(in_repo git ls-tree "$ref" releases) ]] && continue
        # If this branch does not have a .gitmodules, we don't care about it.
        [[ $(in_repo git ls-tree "$ref" .gitmodules) ]] || continue
        debug "Flattening $br into $build"
        mkdir -p "$build_dir"
        # The contents of the extra directly manages how the bits that are
        # staged on the ISO get installed.  We need to refactor how they are coded,
        # but for now just grab what we need out of each build and put it in the
        # appropriate place in .releases/
        for b in change-image extra; do
            in_repo git checkout "$br" -- "$b"
            in_repo mv "$b" "$build_dir"
        done
        # This is an evil, evil hack caused by some inconsistently named
        # barclamp repos. This finds the appropriate .gitmodules for a given
        # branch, reads and parses it as a git config file, and figures out
        # what the real barclamp name is (based on the repo name).
        # All that without touching the working tree.
        read gmode gtype gsha gpath < <(in_repo git ls-tree "$ref" .gitmodules)
        local -A barclamps
        while read line; do
            [[ $line =~ $bc_url_re ]] || continue
            barclamps["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]%.git}"
        done < <(git config --list --file <(in_repo git cat-file blob "$gsha") |sort -k 3 -t .)
        for bc in "${!barclamps[@]}"; do
            if [[ $ref = refs/tags/* ]]; then
                read gmode gtype gsha gpath < <(in_repo git ls-tree "$ref" "$bc")
                [[ $gmode = 160000 && $gtype = commit ]] || \
                    die "Cannot happen in flatten_crowbar. Something Wicked happened to your repos."
                echo "$gsha" > "$build_dir/${barclamps[$bc]}"
            else
                echo "$(build_branch "$build")" >"$build_dir/${barclamps[$bc]}"
            fi
        done
        unset barclamps
        # If there are any json config files in the branch. grab them
        while read gmode gtype gsha gpath; do
            [[ $gtype = blob && $gpath = *.json ]] || continue
            (cd "$build_dir"; git cat-file blob "$gsha" >"$gpath")
        done < <(in_repo git ls-tree "$ref")
    done < <(git show-ref)
    in_repo git rm -rf extra change-image
    in_repo rm -rf extra change-image
    grep -q '/extra' "$CROWBAR_DIR/.gitignore" || {
        echo '/extra' >>"$CROWBAR_DIR/.gitignore"
        echo '/change-image' >>"$CROWBAR_DIR/.gitignore"
        in_repo git add .gitignore
    }
    [[ -d $CROWBAR_DIR/.releases ]] || return 0
    mkdir -p "$CROWBAR_DIR/releases/"
    cp -a "$CROWBAR_DIR/.releases/." "$CROWBAR_DIR/releases/."
    rm -rf "$CROWBAR_DIR/.releases"
    in_repo git add releases
    in_repo git config 'crowbar.build' 'development/master'
    in_repo git commit -m "Added flattened Crowbar metadata."
    switch_release
}

# Transition from flat metadata to git tracked metadata.
git_track_crowbar_metadata() {
    local tmp_metadata="$CROWBAR_DIR/.releases"
    flat_checkout || die "Crowbar metadata is not flat, cannot convert to git tracked!"
    rm -rf "$tmp_metadata"
    mkdir -p "$tmp_metadata"
    (   cd "$tmp_metadata"
        git init .
        git checkout master
        cat >README.crowbar-metadata <<EOF
This is the crowbar release metadata tracking repository.
There is a branch in this repository for each release of Crowbar
that contains all the metadata for that release and the builds that
are part of that release.
EOF
        git add README.crowbar-metadata
        git commit -m "Initial commit")
    local working_release
    while read working_release; do
        echo "Adding $working_release to git release tracking repository"
        cfg_dir="$(release_cfg_dir "$working_release")"
        (cd "$tmp_metadata"; git checkout master; git checkout -b "$working_release")
        cp -a "$cfg_dir/"* "$tmp_metadata"
        (cd "$tmp_metadata"; git add .; git commit -m "Added initial metadata for $working_release")
    done < <(all_releases)
    echo "Conversion to git tracked metadata finished."
    echo "Please run ./dev switch to start using the new git tracked metadata."
    echo "To remove the flat metadata, perform the following steps:"
    echo
    echo "\$ git rm -rf releases"
    echo "\$ git commit -m \"Removing flat metadata in favor of git-tracked release metadata\""
    echo
    echo "After doing this, your releases will be tracked using the git repo based metadata."
}

# Purge submodule information from the master branch.
# Should only be used after you have flattend Crowbar and
# are happy with the results.
purge_submodule_metadata() (
    cd "$CROWBAR_DIR"
    [[ -f .gitmodules ]] || die "Submodules have already been purged."
    submod_re='^submodule\.([^.]+)\.url'
    while read line; do
        [[ $line =~ $submod_re ]] || continue
        line="${BASH_REMATCH[1]}"
        git config --remove-section "submodule.$line"
        [[ $(git ls-tree -r HEAD "$line") = '160000 commit'* ]] || continue
        git rm --cached "$line"
    done < <(git config --list |grep '^submodule')
    git rm .gitmodules
    rm .gitmodules
)

# Unit testing functions:

test_prereqs_installed() {
    local -a missing

    if [[ ! $(find /usr/include -name 'sqlite3.h') ]]; then
        missing+=("sqlite development headers not installed.")
    fi

    local rvm=/usr/local/rvm/scripts/rvm
    [ -f $rvm ] && source $rvm

    for cmd in bundle erlc; do
        if ! command -v $cmd >/dev/null 2>&1; then
            missing+=("$cmd not installed")
        fi
    done

    if [[ $missing ]]; then
        warn "Missing dependencies: ${missing[@]}"
        warn "Please see: https://github.com/crowbar/crowbar/blob/master/doc/devguide/dev-vm.md#setting-up-the-virtual-machine"
        return 1
    fi

    return 0
}

paths_same() {
    [[ $1 && $2 && -d $1 && -d $2 ]] || die "'$1' and '$2' are not directories."
    (   exec >/dev/null 2>/dev/null
        diff -q \
            <(cd "$1"; find -type f -not -path './.git*' |sort |xargs sha1sum -b) \
            <(cd "$2"; find -type f -not -path './.git*' |sort |xargs sha1sum -b)
    )
}

barclamp_needs_reinstall() {
    # $1 = name of the barclamp
    [[ -f $CROWBAR_TEST_DIR/opt/dell/barclamps/$1/crowbar.yml ]] || return 0
    ! paths_same "$CROWBAR_TEST_DIR/opt/dell/barclamps/$1" "$CROWBAR_DIR/barclamps/$1"
}

barclamps_needing_reinstall() {
    local bc barclamps=()
    [[ $@ ]] && barclamps=("$@") || barclamps=($(barclamps_in_build))
    for bc in "${barclamps[@]}"; do
        barclamp_needs_reinstall "$bc" || continue
        echo $bc
    done
}

databases_need_reload() {
    # $@ = list of barclamps to check for new migrations
    local bc barclamps=() p
    [[ $@ ]] && barclamps=("$@") || barclamps=($(barclamps_needing_reinstall))
    for bc in "${barclamps[@]}"; do
        for p in "crowbar_framework/db" "crowbar_framework/test/fixtures" \
            "chef/data_bags"; do
            [[ -d "$CROWBAR_DIR/opt/dell/barclamps/$1/$p" ]] || continue # ignore if dir not there.
            paths_same "$CROWBAR_TEST_DIR/opt/dell/barclamps/$1/$p" \
                "$CROWBAR_DIR/opt/dell/barclamps/$1/$p" && continue
            debug "Database files changed in $bc, need reload."
            return 0
        done
    done
    # and also make sure that we have a database in postgresql database (1 = yes, 0 = no)
    if psql -l postgresql://crowbar@:5439/template1 | grep -q crowbar_development; then
        false
    else
        true
    fi
}

reinstall_barclamps() {
    local barclamps=() barclamp_list=() bc build=$(current_build)
    [[ $@ ]] && barclamps=("$@") || barclamps=($(barclamps_needing_reinstall))
    if [[ ! $barclamps ]]; then
        debug "No barclamps changed."
        return 0
    fi
    mkdir -p "$CROWBAR_TEST_DIR/opt/dell/barclamps"
    for bc in "${barclamps[@]}"; do
        rm -rf "$CROWBAR_TEST_DIR/opt/dell/barclamps/$bc"
        cp -a "$CROWBAR_DIR/barclamps/$bc" "$CROWBAR_TEST_DIR/opt/dell/barclamps"
    done
    debug "${barclamps[*]} barclamps have changed, reloading."
    (   cd "$CROWBAR_TEST_DIR/opt/dell/barclamps"
        "${CROWBAR_DIR}/extra/barclamp_install.rb" \
            --build --force --root "$CROWBAR_TEST_DIR" "${barclamps[@]}") || \
            die "Failed to install some barclamps."

    find "$CROWBAR_TEST_DIR" -type d -exec chmod a+x '{}' ';'
}

validate_data_bags() {
    # This requires the gems to be installed
    cd "$CROWBAR_TEST_DIR/opt/dell/crowbar_framework"
    for d in "$CROWBAR_TEST_DIR/opt/dell/barclamps/"*; do
        [[ -d $d/chef/data_bags ]] || continue
        debug "Validating data bags for $d ..."
        bundle exec "$CROWBAR_TEST_DIR/opt/dell/bin/validate_bags.rb" \
            "$d/chef/data_bags" || \
        die "Crowbar configuration in barclamp ${d##*/} has errors.  Please fix and rerun install."
    done
}

test_env_needs_reset() {
    ! [[ -f $CROWBAR_TEST_DIR/.build && \
        $(cat "$CROWBAR_TEST_DIR/.build") = $(current_build) ]]
}

install_gems() {
    local gemfile="$CROWBAR_TEST_DIR/opt/dell/crowbar_framework/Gemfile"
    if [[ $use_gem_cache = true ]]; then
        maybe_checkout_build_cache_branch
        [[ -d $CROWBAR_TEST_DIR/gemsite ]] && \
            rm -rf "$CROWBAR_TEST_DIR/gemsite/gems"
        mkdir -p "$CROWBAR_TEST_DIR/gemsite/gems"
        find "$CACHE_DIR/barclamps" -type f -name '*.gem' \
            -exec cp '{}' "$CROWBAR_TEST_DIR/gemsite/gems" ';'
        (   cd "$CROWBAR_TEST_DIR/gemsite/"
            gem generate_index)
        sed -i -e "s@^source .*@source \"file://$CROWBAR_TEST_DIR/gemsite\"@" "$gemfile"

        debug "** Installing gems via bundler ..."
        bundle install --path "$CROWBAR_TEST_DIR/gems" --gemfile "$gemfile" || \
            die "Not all required gems are present." \
            "Please run again with --no-gem-cache or --update-gem-cache." \
            "If that does not work, then you will need to add the missing gems to:" \
            "$CROWBAR_DIR/barclamps/crowbar/crowbar.yml"
        debug
    else
        sed -i -e "s@^source .*@source \"https://rubygems.org\"@" "$gemfile"
        echo "NOTE: Using the --no-gem-cache option means that the gems used to"\
        "run the unit tests may not be the same as the ones used to build and"\
        "install Crowbar, so results may differ."

        debug "** Installing gems via bundler ..."
        bundle install --path "$CROWBAR_TEST_DIR/gems" --gemfile "$gemfile" || \
            die "Not all required gems are present."
        debug
    fi
}

setup_test_env() (
    local gem f bc barclamps=()
    local update_gem_cache=false use_gem_cache=true
    for f in "$@"; do
        case $f in
            --update-gem-cache) update_gem_cache=true;;
            --no-gem-cache)     use_gem_cache=false;;
            *) barclamps+=("$f");;
        esac
    done
    test_prereqs_installed || exit 1
    rm -rf "$CROWBAR_TEST_DIR"
    mkdir "$CROWBAR_TEST_DIR"
    reinstall_barclamps "${barclamps[@]}" || exit 1
    cd "$CROWBAR_TEST_DIR"
    if [[ $update_gem_cache = true ]]; then
        maybe_checkout_build_cache_branch
        local crowbar_yml
        for bc in $(barclamps_in_build); do
            crowbar_yml=$(extract_barclamp_metadata "$bc") || \
                die "Cannot extract barclamp metadata for $bc"
            (
                local -A BC_GEMS
                BC_GEMS[$bc]="$("$CROWBAR_DIR/parse_yml.rb" "$crowbar_yml" gems pkgs)"
                update_barclamp_gem_cache "$bc"
            )
        done
    fi
    unset update_gem_cache

    install_gems
    validate_data_bags

    mkdir -p "$CROWBAR_TEST_DIR/doc/framework"
    echo "Copying base docs from $CROWBAR_DIR/doc/. to $CROWBAR_TEST_DIR/opt/dell/doc/framework"
    cp -r "$CROWBAR_DIR/doc/." "$CROWBAR_TEST_DIR/opt/dell/doc/framework"

    debug "Setting up public/coverage symlink"
    ln -sf ../coverage "$CROWBAR_TEST_DIR/opt/dell/crowbar_framework/public"

    cd "$CROWBAR_TEST_DIR/opt/dell/BDD"
    ./linux_compile.sh
    current_build >"$CROWBAR_TEST_DIR/.build"
)

reload_test_env() (
    export PGCLUSTER=9.3/main
    if ! psql postgresql://crowbar@:5439/template1 -c 'select true;' &>/dev/null; then
        echo "Postgres 9.3 is not configured for use by Crowbar!"
        echo "Please consult the dev-tool install instructions"
        echo "verify using: psql postgresql://crowbar@:5439/template1 -c 'select true;'"
        exit 1
    fi
  local rake_task barclamps dbs_need_reload
  if test_env_needs_reset; then
      setup_test_env || exit 1
  fi
  barclamps=($(barclamps_needing_reinstall "$@"))

  if databases_need_reload "${barclamps[@]}"; then
      dbs_need_reload=true
  elif [[ ! $barclamps ]]; then
      return 0
  fi
  reinstall_barclamps "${barclamps[@]}" || exit 1
  validate_data_bags

  if [[ $dbs_need_reload ]]; then
      cd "$CROWBAR_TEST_DIR/opt/dell/crowbar_framework"
      # If you change these, don't forget to also update .travis.yml and
      # doc/devguide/testing!
      for RAILS_ENV in test development; do
          debug "Setting up DB for $RAILS_ENV environment ..."
          (
              export RAILS_ENV
              echo "Dropping database for $RAILS_ENV"
              bundle exec rake db:drop  || :
              echo "Creating database for $RAILS_ENV"
              bundle exec rake db:create || die "Cannot create database for $RAILS_ENV!"
              bundle exec rake railties:install:migrations db:migrate
              bundle exec script/rails generate delayed_job:active_record
              bundle exec rake db:migrate db:fixtures:dump RAILS_ENV=$RAILS_ENV
          )
      done
  fi
  cd "$CROWBAR_TEST_DIR/opt/dell/BDD"
  ./linux_compile.sh
)

run_server() {
  auto_reload_test_env
  cd "$CROWBAR_TEST_DIR/opt/dell/crowbar_framework"
  echo "Starting Rails app & background threads"
  RAILS_ENV=development bundle exec script/delayed_job --queue=NodeRoleRunner -n 2 start
  RAILS_ENV=development bundle exec script/rails s Puma development
  RAILS_ENV=development bundle exec script/delayed_job stop
}

run_console() {
  auto_reload_test_env
  cd "$CROWBAR_TEST_DIR/opt/dell/crowbar_framework"
  echo "Starting Rails console"
  RAILS_ENV=development bundle exec script/rails c
}

run_tests() {
    run_unit_tests
    run_BDD_tests
}

auto_reload_test_env() {
    test_env_needs_reset && clear_test_env
    reload_test_env "$@" || exit 1
}

run_unit_tests() (
    local final_result=pass

    clear # Arkady added this line to fix failing unit tests. Probably better to beef up test_env_needs_reset.

    auto_reload_test_env

    # Run unit tests
    cd "$CROWBAR_TEST_DIR/opt/dell/crowbar_framework"
    rm -rf coverage
    mkdir coverage
    if gem list | grep -q '^rcov '; then
        test_runner=(
            rcov
            --aggregate coverage/testing.data
            --exclude /gems/,/Library/,/usr/,lib/tasks,.bundle,/config/,/lib/rspec/,/lib/rspec-,spec
        )
        spec_runner=( "${test_runner[@]}" -Ispec:lib spec/*/*.rb    )
        unit_runner=( "${test_runner[@]}" -Itest:lib test/unit/*.rb )
    else
        spec_runner=( rake spec )
        unit_runner=( rake test )
    fi
    echo "Running specs ..."
    bundle exec "${spec_runner[@]}" || final_result=fail
    echo "Running unit tests ..."
    bundle exec "${unit_runner[@]}" || final_result=fail

    # chefspec seems to go nuts with chef11... disable for now.
    #echo "Running chef unit tests ..."
    #find -path '*/chef/cookbooks' | xargs -r bundle exec rspec || final_result=fail

    [[ $final_result = pass ]]
)

run_BDD_tests() (
    local final_result=pass
    auto_reload_test_env
    cd "$CROWBAR_TEST_DIR/opt/dell/crowbar_framework"
    echo "Starting Rails app"
    bundle exec script/rails s Puma >> log/test.out 2>> log/test.err &
    sleep 10
    (echo "Running BDD"; cd "$CROWBAR_TEST_DIR/opt/dell/BDD"; erl -s bdd test devtool -s init stop -noshell) || final_result=fail
    echo "Stopping Rails app"
    pkill -f 'ruby.*rails.*Puma'
    [[ $final_result = pass ]]
)

clear_test_env() {
    cd "$CROWBAR_TEST_DIR/opt/dell/crowbar_framework"
    echo "Deleting Database"
    export PGCLUSTER=9.3/main
    RAILS_ENV=development bundle exec rake db:drop
    echo "Removing Test Directory"
    rm -rf "$CROWBAR_TEST_DIR";
    echo "Restarting DB"
    sudo service postgresql restart
}

# Create a new barclamp.
create_barclamp() {
    [[ -d $CROWBAR_DIR/barclamps/$1 ]] && \
        die "Barclamp $1 already exists"
    "$CROWBAR_DIR/extra/barclamp_create.rb" "$@" || exit 1
    (   cd "$CROWBAR_DIR/barclamps/$1"
        git init .
        git add .
        git commit -m "Initial commit"
    )
}

erase_feature() { erase_release "feature/$1"; }

# Tags a build.  The exact steps are:
# 1: Pin the indicated build.
# 2: Tag the freshly-pinned branches with the passed tag
# 3: Unpin the build.
# 4: Push the freshly-created tags to the default upstreams.
tag_build() {
    # $1 = build to tag.
    crowbar_is_clean || die "Crowbar must be clean before tagging a release!"
    [[ $(in_repo git symbolic-ref HEAD) = refs/heads/master ]] || \
        die "Main Crowbar repo must be on the master branch before tagging a release!"
    local build="${1:-$(current_build)}"
    build_exists "$build" || die "Build $build does not exist!"
    local release="${build%/*}" release_br=$(release_branch "$release")
    local tag tag_msg_file bc
    read -erp "Enter tag name: " tag
    [[ $tag =~ ^[a-zA-Z0-9_.-]+$ ]] || die "Invalid tag name." \
        "Tags must only contain [a-zA-Z0-9._-]"
    in_repo git show-ref "refs/tags/$tag" &>/dev/null && \
        die "Tag $tag has already been created!"
    release_exists "$tag" && \
        die "Release for $tag has already been cut!"
    tag_msg_file="$(mktemp /tmp/tag-message-XXXXXXXX)" || \
        die "Cannot create temporary file for tag message!"
    trap "rm -f '$tag_msg_file' || :" RETURN EXIT
    cat >"$tag_msg_file" <<EOF

# Enter a tag annotation message.
EOF
    ${EDITOR:-nano} "$tag_msg_file"
    for bc in $(barclamps_in_build "$build"); do
        in_barclamp "$bc" git tag -a -F "$tag_msg_file" "$tag" \
            "$(barclamp_branch_for_build "$build" "$bc")"
    done
    # Evil, pure evil.
    old_master="master-$$"
    tag_br="tag-master-$tag"
    in_repo git branch "$old_master" "master"
    if in_repo git reset --hard "$(in_repo origin_remote)/master" && \
        cut_tagged_release "$tag" "$build" && \
        in_repo git tag -a -F "$tag_msg_file" "$tag" && \
        in_repo git branch "$tag_br" && \
        in_repo git reset --hard "$old_master" && \
        in_repo git rebase "$tag_br"; then
        in_repo git branch -D "$old_master"
    else
        in_repo git rebase --abort
        in_repo git checkout -f master
        git reset --hard "$old_master"
        git branch -D -- "$tag_br" "$old_master"
        die "Could not stage tag of the build system." \
            "Please fix things up and try again."
    fi
    if git_managed_cache && in_cache branch_exists "$release_br"; then
        in_cache git tag -a -F "$tag_msg_file" "$tag" "$release_br"
    fi
    debug "Build $build tagged." \
        "You can push the tags with ./dev push-tag $tag"
}


# Push specified tag to its "best" upstream, or
# whatever DEV_TO_REMOTES says to push to.
# This function tries to ensure that it will fail without doing anything
# if there will be any problems (due to authetication or whatever).
push_tag() {
    local rel_br release remotes remote bc res cmd build
    local pushcmds=()
    in_repo branch_exists "tag-master-$1" || \
        die "Tag $1 does not have a local staging branch." \
        "push-tag only knows how to push tags created with tag-build."
    [[ $DEV_TO_REMOTES ]] || \
        die "You must pass the remotes to push the tag to using --to."
    for remote in "${DEV_TO_REMOTES[@]}"; do
        remote_available "$remote" || continue
        for bc in "$CROWBAR_DIR/barclamps/"*; do
            [[ -f $bc/.git || -d $bc/.git ]] || continue
            bc=${bc##*/}
            # Skip any barclamps that do not have the tag in question.
            in_barclamp "$bc" git show-ref refs/tags/"$1" &>/dev/null || continue
            # If the tag exists but is not an annotated tag, die horribly
            in_barclamp "$bc" git cat-file tag "$1" &>/dev/null || \
                die "Tag $1 exists in barclamp $bc, but it is not an annotated tag!" \
                "Please fix this up and try again."
            if ! probe_barclamp_remote "$bc" "${DEV_REMOTE_URLBASE[$remote]}" &>/dev/null; then
                debug "Barclamp $bc does not exist at $remote, skipping."
                continue
            fi
            if ! res="$(in_barclamp "$bc" git push -nq "$remote" -- tag "${1}")"; then
                debug "$bc: tag $1 -> $remote will fail."
                echo "$res" >&2
                exit 1
            fi
            debug "$bc: tag $1 -> $remote is OK."
            pushcmds+=("in_barclamp $bc git_push $remote -- tag $1")
        done
        if ! res="$(in_repo git push -nq "$remote" -- tag "$1")"; then
            debug "Crowbar: tag $1 -> $remote will fail."
            echo "$res" >&2
            exit 1
        elif ! res="$(in_repo git push -nq "$remote" -- "tag-master-$1:master")"; then
            debug "Crowbar: adding proper release information to master on $remote will fail."
            echo "$res" >&2
            exit 1
        fi
        debug "Crowbar: tag $1 and master updates -> $remote OK."
        pushcmds+=("in_repo git push $remote -- tag $1" "in_repo git push $remote -- tag-master-$1:master")
    done
    debug "Test passed, pushing tags."
    for cmd in "${pushcmds[@]}"; do
        if [[ $DRY_RUN = true ]]; then
            echo "Would have run: $cmd"
        else
            $cmd || die ""
        fi
    done
    [[ $DRY_RUN = true ]] || in_repo git branch -D "tag-master-$1"
}

# Wrapper around common pull request functionality.
pull_request_handler() {
    local req=$1
    shift
    case $req in
        fetch)       fetch_pull_requests_for_remote "$@";;
        prep)        pull_requests_prep "$@";;
        gen)         pull_requests_gen "$@";;
        dirs)        pull_request_directories "$@";;
        list)        list_pull_requests "$@";;
        show)        show_pull_request "$@";;
        builds)      builds_for_pull_request "$@";;
        switch)      pull_request_switch "$@";;
        merge)       pull_request_merge "$@";;
        multiswitch) pull_request_multiswitch "$@";;
        checkout)    pull_request_checkout "$@";;
        comment)     pull_request_comment "$@";;
        close)       pull_request_close "$@";;
        '')          show_open_pull_requests "$@";;
        *)           dev_help 'pull-requests';;
    esac
}

tests_handler() {
    local req=$1
    shift
    case $req in
        setup)     setup_test_env          "$@" ;;
        run)       run_tests               "$@" ;;
        run-units) run_unit_tests          "$@" ;;
        run-BDD)   run_BDD_tests           "$@" ;;
        server)    run_server              "$@" ;;
        console)   run_console             "$@" ;;
        reload)    reload_test_env         "$@" ;;
        clear)     clear_test_env          "$@" ;;
        *)         dev_help tests               ;;
    esac
}

ci_helper() {
    local req=$1
    shift
    case $req in
        fetch) fetch_ci_tracking "$@";;
        import) ci_import_new_pull_requests "$@";;
        push) push_ci_tracking "$@";;
        reset)
            ci_do_and_push ci_reset_pull_request "$@";;
        states) ci_get_current_states "$@";;
        all-next-states) ci_all_next_states "$@";;
        next-states) ci_next_states "$@";;
        open-ids) ci_open_ids "$@";;
        failed-ids) ci_filtered_ids failed needs-work;;
        filtered-ids) ci_filtered_ids "$@";;
        all-open-ids) ci_ids "$@";;
        register-intent)
            ci_do_and_push ci_register_intent "$@";;
        commit-intent) ci_commit_intent "$@";;
        expire-intent) ci_expire_intent "$@";;
        delete-intent) ci_delete_intent "$@";;
        show-intents) ci_show_intents "$@";;
        fail-intent) ci_fail_intent "$@";;
        kill-stale-intents)
            ci_do_and_push ci_kill_stale_intents "$@";;
        *)
            dev_help 'ci'
            exit 1 ;;
    esac
}
# Hash for handling command -> function mapping.
DEV_COMMANDS["backup"]="backup_everything"
DEV_COMMANDS["barclamps-in-build"]="barclamps_in_build"
DEV_COMMANDS["branch"]="current_build"
DEV_COMMANDS["branches"]="builds_in_release"
DEV_COMMANDS["build"]="build_crowbar"
DEV_COMMANDS["build-refs"]="refs_for_build"
DEV_COMMANDS["builds"]="builds_in_release"
DEV_COMMANDS["checkout"]="checkout"
DEV_COMMANDS["ci"]="ci_helper"
DEV_COMMANDS["clear-unit-tests"]="clear_test_env"
DEV_COMMANDS["clone-barclamps"]="clone_barclamps"
DEV_COMMANDS["create-barclamp"]="create_barclamp"
DEV_COMMANDS["crowbar-version"]="crowbar_version"
DEV_COMMANDS["current-build"]="current_build"
DEV_COMMANDS["cut_release"]="cut_release"
DEV_COMMANDS["erase-feature"]="erase_feature"
DEV_COMMANDS["fetch"]="fetch_all"
DEV_COMMANDS["fetch-pull-requests"]="fetch_pull_requests_for_remote"
DEV_COMMANDS["find-parent"]="find_best_parent"
DEV_COMMANDS["flatten"]="flatten_crowbar"
DEV_COMMANDS["gitify-meta"]="git_track_crowbar_metadata"
DEV_COMMANDS["help"]="dev_help"
DEV_COMMANDS["is_clean"]="crowbar_is_clean"
DEV_COMMANDS["local-changes"]="find_local_changed_branches_for_release"
DEV_COMMANDS["merge"]="merge_releases"
DEV_COMMANDS["missing-barclamps"]="find_missing_barclamps"
DEV_COMMANDS["new-feature"]="new_feature"
DEV_COMMANDS["orphaned-barclamps"]="find_orphaned_barclamps"
DEV_COMMANDS["pin"]="pin_release"
DEV_COMMANDS["pull-requests"]="pull_request_handler"
DEV_COMMANDS["pull-requests-gen"]="pull_requests_gen"
DEV_COMMANDS["pull-requests-prep"]="pull_requests_prep"
DEV_COMMANDS["purge-submodules"]="purge_submodule_metadata"
DEV_COMMANDS["push"]="push_branches"
DEV_COMMANDS["push-release"]="push_release"
DEV_COMMANDS["push-tag"]="push_tag"
DEV_COMMANDS["redundant-barclamps"]="show_duplicate_barclamps"
DEV_COMMANDS["refresh-tracking"]="update_all_tracking_branches"
DEV_COMMANDS["release"]="current_release"
DEV_COMMANDS["releases"]="show_releases"
DEV_COMMANDS["reload-unit-tests"]="reload_test_env"
DEV_COMMANDS["remote"]="remote_wrapper"
DEV_COMMANDS["remotes"]="remote_wrapper show"
DEV_COMMANDS["reset-release"]="reset_release"
DEV_COMMANDS["run-tests"]="run_tests"
DEV_COMMANDS["run-BDD-tests"]="run_BDD_tests"
DEV_COMMANDS["run-unit-tests"]="run_unit_tests"
DEV_COMMANDS["scrub-merged-pulls"]="scrub_merged_pulls"
DEV_COMMANDS["setup"]="setup"
DEV_COMMANDS["setup-unit-tests"]="setup_test_env"
DEV_COMMANDS["switch"]="switch_release"
DEV_COMMANDS["sync"]="sync_everything"
DEV_COMMANDS["tag-build"]="tag_build"
DEV_COMMANDS["tests"]="tests_handler"
DEV_COMMANDS["unpin"]="unpin_release"
DEV_COMMANDS["upstream-changes"]="find_remote_changed_branches_for_release"
DEV_COMMANDS["bash_completion"]="dev_bash_completion"

# Hash for short help
DEV_SHORT_HELP["backup"]="             Back up changes without merging them into upstream."
DEV_SHORT_HELP["barclamps-in-build"]=" Shows the barclamps that are members of a build."
DEV_SHORT_HELP["branch"]="             Show the build you are working on. Synonym for build with no args."
DEV_SHORT_HELP["branches"]="           Show the builds in the current release.  Synonym for builds."
DEV_SHORT_HELP["build"]="              Show the current build or build Crowbar"
DEV_SHORT_HELP["builds"]="             Show the builds in the current release."
DEV_SHORT_HELP["build-refs"]="         Show the Git commits that would be used in a build."
DEV_SHORT_HELP["checkout"]="           Check out a branch in the current release."
DEV_SHORT_HELP["ci"]="                 Helper for Continuous Integration subcommands."
DEV_SHORT_HELP["clear-unit-tests"]="   [deprecated] same as ./dev tests clear"
DEV_SHORT_HELP["clone-barclamps"]="    Clone barclamps."
DEV_SHORT_HELP["create-barclamp"]="    Create a new barclamp."
DEV_SHORT_HELP["crowbar-version"]="    Print out a probably-unique ordered version string."
DEV_SHORT_HELP["current-build"]="      Shows the current build you are working on."
DEV_SHORT_HELP["cut_release"]="        Cut a new release from the current one."
DEV_SHORT_HELP["erase-feature"]="      Forget about branches for a feature you are not working on."
DEV_SHORT_HELP["fetch"]="              Fetch updates from configured upstream repositories."
DEV_SHORT_HELP["fetch-pull-requests"]="Fetch pull request information from a specific remote"
DEV_SHORT_HELP["find-parent"]="        Find the closest parent of a release or feature."
DEV_SHORT_HELP["flatten"]="            Flatten the main Crowbar repo branching structure."
DEV_SHORT_HELP["gitify-meta"]="        Take flattened Crowbar metadata and put it in its own Git repo."
DEV_SHORT_HELP["help"]="               Show detailed help."
DEV_SHORT_HELP["is_clean"]="           Test to see if all work is committed."
DEV_SHORT_HELP["local-changes"]="      Find local changes from upstream for a release"
DEV_SHORT_HELP["merge"]="              Merge other releases into the current release"
DEV_SHORT_HELP["missing-barclamps"]="  Show missing barclamps."
DEV_SHORT_HELP["new-feature"]="        Create a new feature bundle from the current release."
DEV_SHORT_HELP["orphaned-barclamps"]=" Show barclamps that are not part of any build."
DEV_SHORT_HELP["pin"]="                Pin a release, build, or barclamp in a build to a specific rev"
DEV_SHORT_HELP["pull-requests"]="      Manage creating, testing, and merging pull requests"
DEV_SHORT_HELP["pull-requests-gen"]="  Issue pull requests."
DEV_SHORT_HELP["pull-requests-prep"]=" Prepare to issue pull requests."
DEV_SHORT_HELP["purge-submodules"]="   Purge submodule information for the master branch in Crowbar."
DEV_SHORT_HELP["push"]="               Unconditionally push a branch to your personal repos."
DEV_SHORT_HELP["push-release"]="       Push a release out to a remote"
DEV_SHORT_HELP["push-tag"]="           Push tags for a tagged release to specified remotes."
DEV_SHORT_HELP["redundant-barclamps"]="Show redundant barclamp metadata in a release"
DEV_SHORT_HELP["refresh-tracking"]="   Update git branch tracking metadata."
DEV_SHORT_HELP["release"]="            Show the current release or feature you are on."
DEV_SHORT_HELP["releases"]="           Show all releases and features in your local repos."
DEV_SHORT_HELP["reload-unit-tests"]="  [deprecated] same as ./dev tests reload"
DEV_SHORT_HELP["remote"]="             Manage remotes across all Crowbar repositories."
DEV_SHORT_HELP["remotes"]="            Show all remotes."
DEV_SHORT_HELP["reset-release"]="      Resets a release to upstream or your last backup."
DEV_SHORT_HELP["run-tests"]="          [deprecated] same as ./dev tests run"
DEV_SHORT_HELP["run-unit-tests"]="     [deprecated] same as ./dev tests run-units"
DEV_SHORT_HELP["run-BDD-tests"]="      [deprecated] same as ./dev tests run-BDD"
DEV_SHORT_HELP["scrub-merged-pulls"]=" Scrub merged pull request branches."
DEV_SHORT_HELP["setup-unit-tests"]="   [deprecated] same as ./dev tests setup"
DEV_SHORT_HELP["setup"]="              Sets up a Crowbar checkout for use with dev."
DEV_SHORT_HELP["switch"]="             Switch to a different release."
DEV_SHORT_HELP["sync"]="               Synchronize fetched updates with the local repos."
DEV_SHORT_HELP["tag-build"]="          Add annotated tags to all components of a particular build."
DEV_SHORT_HELP["tests"]="              Various tasks relating to running tests."
DEV_SHORT_HELP["unpin"]="              Unpin a release, build, or barclamp in a build."
DEV_SHORT_HELP["upstream-changes"]="   Find unmerged changes from upstream for a release"
DEV_SHORT_HELP["bash_completion"]="    Make tab ~/.bash_completion for dev (overwrites)"

# Hash for long help
DEV_LONG_HELP["merge"]="Merge other releases into the current release
  This command interprets any arguments passed on the command line as
  the names of releases to be merged into the current release."
DEV_LONG_HELP["bash_completion"]="Enable tab command completion.
  Overwrites /\$HOME/.bash_completion every time you run it. Inserts
  all the implemented commands.  Not yet subcommands.  You need to
  logout and login for it to function."
DEV_LONG_HELP["setup"]="Perform initial setup of Crowbar.
  Sets up your local Crowbar repositories for working with the new
  Github regime.  By default, This command will prompt for your Github
  login information, make sure you have local copies of all the
  barclamp repos that Crowbar references checked out, create personal
  forks of all the crowbar repos from the $(origin_remote) account on
  github, and create the appropriate remotes needed for day-to-day
  operation.

  If you pass the --no-github flag to setup, it will not try to create
  personal forks for your barclamps or try to save your github id and
  password."
DEV_LONG_HELP["is_clean"]="Check to make sure the repos are clean.
  Check to see if the barclamps have everything committed with no
  untracked files, and check to see if the main Crowbar repository is
  clean while ignoring submodule references."
DEV_LONG_HELP["fetch"]="Fetch changes from remote repositories.
  Fetch nondestructivly fetches changes from all reachable remote
  repositories by default, without making changes to your local
  working trees. You can run a fetch at any time without disturbing
  your working code, provided you have network connectivity.  If you
  pass the names of remotes, or specify remotes using the --from flag,
  fetch will try to fetch changes from just those remotes.

  If you are experiencing lots of !!!! in your fetches, then export
  BADLINK=true and dev will run fetch serially, not in parallel.
  Also, the temporary files will not be removed, so you can report
  on the failures later."
DEV_LONG_HELP["remote"]="Manage remotes across all repositories.
  Subcommands:
    add <remote> <urlbase> : The add subcommand adds a new remote to
      the Crowbar repositories.
    rm <remote> : The rm subcommand removes an already-existing remote
      from the Crowbar repositories.
    rename <old_name> <new_name> : Rename the remote named <old_name>
      to <new_name> in all the Crowbar repositories.
    set-url <remote> <urlbase> : The set-url subcommands allows you to
      give an existing remote a new URL in all the subrepositories.
    show <remote> : The show subcommand shows remote information for
      the named remote in all the Crowbar repositories.
    sync : Synchronize configured remotes from the main Crowbar repository
      to any cloned barclamps.
    priority <remote> <number>: Set the priority of a remote.
      This controls how local branches bind to remote branches -- any
      local branches that have a corresponding branch in a remote will
      be configured to track the remote with the smallest priority.
      By convention, the repository you cloned from will have a
      priority of 5, the personal repositiry on Github will have a
      priority of 95, and everything else will have a priority of 50.
      You can change the priority of a remote at any time, and your
      local tracking branches will be updated to reflect the new
      remote priority list.

  Note that <urlbase> is the URL git will use without the actual
  repository name.  dev assumes that the new remote uses the same
  repository name as the origin remote does, and things will fail
  badly if this assumption is violated.  As an example, the urlbase
  for:
    https://github.com/crowbar/crowbar.git
  is:
    https://github.com/crowbar"
DEV_LONG_HELP["remotes"]="Show all remotes."
DEV_LONG_HELP["backup"]="Back up local changes to the personal remote.
  Push any locally committed changes into repositories that you have
  initially forked from $(origin_remote) into your personal Github
  forks.  You can run a backup at any time without disturbing your
  working code, provided you have network connectivity."
DEV_LONG_HELP["sync"]="Synchronize local branches with their
  upstreams.  Sync merges changes (fetched using the fetch command)
  from their respecitve remote tracking branches into their local
  branches.  sync may choose to rebase local changes on top of changes
  from upstream, as long as there are no rebase or merge conflicts and
  the results of the rebase are identical to the results of the
  equivalent merge.  dev is_clean should exit without any messages
  before running this command."
DEV_LONG_HELP["push"]="Push a branch or branches to your personal remote.
  Unconditionally push a branch (or branches) to your crowbar Github
  fork.  Any arguments are interpreted as branch names to push, and if
  there are no arguments it pushes the branch you are currently on.
  Any branches pushed using this command will automatically be backed
  up when dev backup is run as well."
DEV_LONG_HELP["pull-requests-prep"]="Alias for pull-requests prep"
DEV_LONG_HELP["pull-requests-gen"]="Alias for pull-requests gen"
DEV_LONG_HELP["release"]="Shows the current release that dev is operating on."
DEV_LONG_HELP["releases"]="Lists the releases to choose from."
DEV_LONG_HELP["builds"]="Show builds in the current or specified release.
  Shows the builds that are part of the specified release, or the
  current release if no release is specified."
DEV_LONG_HELP["current-build"]="Shows the build that the barclamps are set to."
DEV_LONG_HELP["barclamps-in-build"]="Show barclamps in the current or specified build.
  Shows all the barclamps that are members of the build passed as the
  first argument, or of the current build if no argument is passed."
DEV_LONG_HELP["switch"]="Switch to a new release or release/build.
  Change current release to the specified release, or the current
  release if no release name was passed.  This has the side effect of
  making sure that all the barclamps are on the proper branch for that
  release. dev switch will also set any barclamps that are not used in
  the new branch to an empty branch."
DEV_LONG_HELP["checkout"]="Check out a new build in the current release.
  Change to a new build in the current release.  Checkout will ensure
  that any barclamps that are ont being used by the new branch are set
  to an empty branch."
DEV_LONG_HELP["build"]="Build Crowbar or display the current working build.
  If no arguments are passed to this command, it displays the current
  build.  Otherwise, it will perform some local sanity checks and
  build Crowbar based on the current release.
  Parameters:
    --os = operating system to stage Crowbar on to.
    --release = Release or release/build of Crowbar to build.
    --no-switch = Do not try and sanity-check the branches.
      This is usefil when testing builds based on a pull request.
  Any other parameters will be passed unchanged to build_crowbar.sh.
  See README.build for more information on building Crowbar."
DEV_LONG_HELP["cut_release"]="Cut a new release of Crowbar.
  DO NOT USE UNLESS YOU KNOW WHAT YOU ARE DOING!  Makes a new set of
  branches and barclamp branches from the current release into the new
  named release.  Must provide a unique new name."
DEV_LONG_HELP["new-feature"]="Create a new set of feature branches.
  You must provide a unique new name, and you must be in the
  development release when you run this command.  Internally, features
  are implemented as releases in the feature/ namespace, so all the
  other release manipulation commands will work with them."
DEV_LONG_HELP["erase-feature"]="Erase feature branches created with new-feature.
  This command is intended to be run after a feature has been merged
  into the main devleopment stream to avoid cluttering up the output
  of the git branch command.  It will fail if the feature has not been
  merged into the development release."
DEV_LONG_HELP["find-parent"]="Display the parent release of the current feature.
  Find the parent of a release or feature based on the which of the
  releases have the shortest merge distance from the release passed to
  find-parent, so it is possible for more than one release to be
  considered to be the parent. If that turns out to be the case,
  find-parent will ask you to disambiguate."
DEV_LONG_HELP["reset-release"]="Reset a release or feature to the last backup or upstream.
  Paremeters:
    --release: Release to reset.  Defaults to current.
    --target: Either 'upstream' or 'backup'. Defaults to 'backup'.
  This command uses git reset --hard on the current release branches
  in the barclamps, so you can lose your commits unless you take
  precautions."
DEV_LONG_HELP["pin"]="Pin a release, build, or barclamp in a build..
  Normally, barclamps in a build are allowed to track the appropriate
  branches for a release.  Pin allows you to either force them to
  track a nonstandard branch, or lock them at a specific commit or
  tag.  The first argument to pin is one of a release, a build in a
  release, or a barclamp in a release in the following formats:
    <release>
    <release>/<build>
    <release>/<build>/<barclamp>
  The second argument to pin is the branch or tag to pin the barclamps
  to.  If nothing is passed, pin will pin the barclamps at their
  current HEAD."
DEV_LONG_HELP["unpin"]="Undo the effect of a pin.
  Unpin has barclamps track the default branch for the passed release.
  The first argument to unpin is the same as for pin.  There is no
  second argument."
DEV_LONG_HELP["scrub-merged-pulls"]="Scrub merged pull request tracking branches.
  Delete any tracking branches created as placeholders for pull
  requests.  Without any options, this just scrubs tracking branches
  for pull requests that have already been merged.  If --all is passed
  as an option, then all pull request tracking branches will be
  deleted."
DEV_LONG_HELP["tests"]="Various tasks related to running tests.
  Subcommands:
    setup
      This will create the environment with all barclamps in
      /tmp/crowbar-dev-test.  By passing in names of barclamps, the
      setup command will only add those barclamps to the test
      environment.

      setup also takes two optional arguments:
        --update-gem-cache will copy the contents of /var/lib/gems/1.9.1 into
            $CACHE_DIR/unit_tests/gems.  If you are managing your build cache
            with git, these files will be automatically committed.
        --no-gem-cache will force bundler to use gems from the internet
            instead of trying to use files saved by --update-gem-cache.
    reload
      Reload the code and database for the units.  Does not re-bundle.
    run
      Run all tests from the /tmp/crowbar environment.
    run-BDD
      Run BDD tests from the /tmp/crowbar environment.
    run-units
      Run unit tests and specs from the /tmp/crowbar environment.
    server
      Run the dev web server interactively
    console
      Run the dev console interactively
    clear
      Removes a test environment in /tmp/crowbar."
DEV_LONG_HELP["setup-unit-tests"]="Deprecated; please see help for ./dev tests setup"
DEV_LONG_HELP["reload-unit-tests"]="Deprecated; please see help for ./dev tests reload"
DEV_LONG_HELP["run-tests"]="Deprecated; please see help for ./dev tests run"
DEV_LONG_HELP["run-BDD-tests"]="Deprecated; please see help for ./dev tests run-BDD"
DEV_LONG_HELP["run-unit-tests"]="Deprecated; please see help for ./dev tests run-units"
DEV_LONG_HELP["clear-unit-tests"]="Deprecated; please see help for ./dev tests clear"
DEV_LONG_HELP["refresh-tracking"]="Update remote tracking information for all the branches.
  Update remote tracking information for local branches in all the
  repos.  This command is run internally whenever dev does anything that
  updates remote availablity and priority or that might add or remove
  branches from any of the repositories.  It updates git's per-branch
  remote tracking metadata to reflect the current remote priority
  order."
DEV_LONG_HELP["clone-barclamps"]="Clone barclamps.
  Clone any barclamps that may be missing from the current
  environment, and update the remote mappings in any that do
  exist. With no arguments, all barclamps from all releases will be
  considered for cloning.  With a release or release/build, barclamps
  from just that release or build will be considered."
DEV_LONG_HELP["branch"]="Shows the build you are currently working on."
DEV_LONG_HELP["purge-submodules"]="Purge submodule information from the master branch in the main Crowbar repository.
  This should only be run after $0 flatten has been run, and you are satisfied with the results."
DEV_LONG_HELP["help"]="Shows long help.
  Takes an optional dev command to show help on.
  dev help all shows the long help for all the commands."
DEV_LONG_HELP["flatten"]="Flatten the Crowbar repository
  Migrate a Crowbar repository from:
    Using a hierarchical branching structure to define releases and
    builds with submodule references on the tips of the branches to
    track what barclamps are part of what build
  to:
    Using a directory named releases in the master branch of the main
    Crowbar repo to track release/branch to barclamp mappings.
  If you are not responsible for maintaining a Crowbar repository for
  your team, you should not need to use this command."
DEV_LONG_HELP["orphaned-barclamps"]="Show barclamps that are not part of any build."
DEV_LONG_HELP["missing-barclamps"]="Show barclamps that are referenced by a build, but not present locally.
  This is usually caused by a build requiring a new barclamp.  You can
  pull in missing barclamps with clone-barclamp if the barclamp is in
  one of your configured remotes."
DEV_LONG_HELP["create-barclamp"]="Create a new barclamp, but do not register it with a build.
  The only argument is the name of the new barclamp, and the barclamp
  will be created in $CROWBAR_DIR/barclamps."
DEV_LONG_HELP["branches"]="Synonym for builds."
DEV_LONG_HELP["upstream-changes"]="Find unmerged changes from upstream for a release.
  Scan through each barclamp looking for unmerged changes for the
  passed release, or the current release if none was passed.  By
  default, this uses the tracking branches to determine what the
  upstream remote is for each branch, but this can be overridden with
  the --from parameter.

  This command operates locally -- you need to run dev fetch to ensure
  that this command is accurate.  If any changes are found, it will
  list the barclamp, local, and remote branches involved."
DEV_LONG_HELP["local-changes"]="Find local changes for a release
  Scan through each barclamp looking for local changes for the
  passed release, or the current release if none was passed.  By
  default, this uses the tracking branches to determine what the
  upstream remote is for each branch, but this can be overridden with
  the --from parameter.

  This command operates locally -- you need to run dev fetch to ensure
  that this command is accurate.  If any changes are found, it will
  list the barclamp, local, and remote branches involved."
DEV_LONG_HELP["push-release"]="Push the named release to the
  specified remote.  Test to see of all the branches that comprise a
  release across all the branches can be pushed out to either the
  default remote, or the remotes passed via the --to arguments.  Aside
  from --to, the only parameter is the name of a release, or the
  current release of no release is passed.

  This command will perform test pushed for all the branches in the
  release to all the specificed remotes.  If any of the tests fail,
  nothing will be pushed."
DEV_LONG_HELP["crowbar-version"]="Print out a probably-unique mostly sequential version"
DEV_LONG_HELP["build-refs"]="Show the Git commits that would be used in a build."
DEV_LONG_HELP["redundant-barclamps"]="Show redundant barclamp metadata for a build."
DEV_LONG_HELP["tag-build"]="Add an annotated tag to all components of a build.
  This command adds annotated tags objects for the passed build,
  or the current build if no build was passed.  It will prompt for the
  tag name and drop you into an editor for the tag message."
DEV_LONG_HELP["push-tag"]="Pushes a tagged release to specified remotes.
  The tagged release must have been created with tag-build.
  First argument must be the tag created with tag-build, and the remotes to push to
  must be passed explicitly with --to."
DEV_LONG_HELP["fetch-pull-requests"]="Fetch pull request metadata from a remote"
DEV_LONG_HELP["pull-requests"]="Manage pull requests.
  Subcommands:
    fetch: Fetch open pull requests from all repositories at a specific remote.
      By default, dev fetch will also grab pull request information, and
      display the number of open pull requests and pull request bundles
      after all other fetch operations have finished.
    prep: Prepare to issue pull requests.
      Fetches, Syncs, and backs up all changes, figures out the set of
      barclamps and crowbar branches in the current release that have
      local changes, and generates a commandline based on the assumption
      that you want to issue pull requests for all the changes it found.

      pull-requests-prep honors the --to flag to control what remote you
      want to send your pull requests to.  It also honors a --merge flag
      to signal that you want to merge the current release into its parent.
      This lets you use feature branches as topic branches.

      The output is a command line that can be edited to reflect what you
      actually want to issue pull requests for.
    gen: Generate pull requests to Github.
      Creates a set of pull requests based on differences inthe current
      release between your origin remote and your local trees.

      If you pass the --merge flag to pull-requests gen, it will generate
      pull requests that will merge the current release into its parent
      release.  This is intended to facilitate using feature bundles as
      short-lived topic branches.
    list: List the open pull requests an pull request bundles.
      List displays the pull requests in oldest-first format, and the
      numbers next to each pull request will be stable until the next
      dev fetch or dev pull-requests fetch operation happens.
    show: Show details about a pull request or pull request bundle.
    comment: Add a comment to a pull request or to each pull request in a
      bundle. First arg is the ID, second arg is a one-line comment.
    merge: Have Github merge the pull request or bundle.
    close: Close a pull request or pull request bundle
      Second arg must be a closing comment.
    builds: Show a list of builds this pull request should trigger.
    checkout: Check out the exact contents of a pull request.
    switch: Switch to a new tree that has the contents of a pull request
      merged into your current trees.  Your trees must be clean before
      doing this. The first argument to this command must be a pull request
      number, and the second argument must be a build that is applicable to
      the pull request.

      To build in this state, you must: ./dev build --no-switch

      Please note that this will put any trees that have changes to be pulled
      into a detached HEAD state.  You can use dev switch to get back to normal.
    multiswitch: Switch to a new tree that has the contents of several pull
      requests merged into it for expedited testing.

      The first argument must be the build you want to test, and
      the rest of the arguments must be pull requests IDs."
DEV_LONG_HELP["ci"]="Crowbar Continuous Integration Helper.
  Any of the CI subcommands that take an ID can either take a raw ID as
  returned by dev ci all-ids,
  or a pull request number as returned by dev pull-requests list, although the
  latter will only work for pull request bundles that are imported and not closed.
  Subcommands:
    fetch: Pull the latest CI tracking metadata from the upstream repository.
      This will attempt to rebase any local state changes on top of changes
      from upstream.  If that fails, it will throw away your local changes and
      exit with a non-zero exit status.
    push: Performs a fetch, tests to see if there is any local state that
      is not in the just-pulled state, and tries to push the local state
      upstream if there is any local state.
    import: Imports the current state of tracked pull requests from their
      Github pull requests, closing and merging pulls in the CI state database
      as needed. It will then reset any pull requests whose underlying Git
      branches have been updated, and import any pull requests that are not
      already being tracked by the CI database.
    reset: Resets the state machine for a specific pull request ID back to new.
    open-ids: Lists the IDs of all open pull requests and pull request bundles.
      These ids are stable and globally unique.
    failed-ids: Lists the IDs of all the failed pull requests.
    filtered-ids: Lists the IDs of all pull requests in a given state.
    all-open-ids: List the IDs of all the pull requests and pull request bundles
      that the CI system knows about.
    states: Shows the current states that a pull request or pull request bundle
      is at in the CI system.
      The only argument is the ID of the pull request.
    all-next-states: Shows all the next states that may be applicable to
      the current or passed state of a pull request.
      Arguments:
        id: The ID of the pull request.
        (optional) state: The state to determine the next state from. Defaults
          to the current state of the pull request, if the pull request is
          only in one state.
    next-states: Same as all-next-states, but filters out state transitions
      that have an intent registered for them in the state machine.
    register-intent: Register an intent to do whatever work is needed to
      run the tests and do the work needed to transition a pull request
      from one state to another.
      Arguments:
        id: A pull request ID.
        current_state: One of the current states of the pull request.
        next_state: The intended next state.  This must be one of the states
          that next-states would return for current_state.
        timeout: How many seconds from now the CI system can discard this
          intent.  Defaults to 5 hours, but it should be set to a smaller value
          if you expect your task to take a shorter amount than this.
      Returns a unique ID for the intent.
    commit-intent: Tell the CI system that the test we wanted to run for a
      registered intent passed, and that it should transition appropriatly.
      Arguments:
        id: An intent ID returned by register-intent.
    fail-intent: The same as commit-intent, but it will transition to the
      failed state instead.
    expire-intent: Immediatly force an intent to go stale.
    delete-intent: Deletes an intent.  This potentially prunes a path from
      the state transition tree.
    show-intents: Shows all the current and stale intents for a pull request.
    kill-stale-intents: Given a pull request ID, this will kill any stale intents
      that are hanging around and attempt to push them out."
DEV_LONG_HELP["gitify-meta"]="Translate flattened Crowbar metadata from the releases/
directory hierarchy, and put it in its own Git repository."

# Verify that we have help for all commands, and exit if we don't.
for cmd in "${!DEV_COMMANDS[@]}"; do
    [[ ${DEV_SHORT_HELP[$cmd]} && ${DEV_LONG_HELP[$cmd]} ]] && continue
    [[ ${DEV_SHORT_HELP[$cmd]} ]] || echo "Missing short help for $cmd"
    [[ ${DEV_LONG_HELP[$cmd]} ]] || echo "Missing long help for $cmd"
    ret=1
done
[[ $ret ]] && exit 1
unset cmd ret
set_sorted_remotes

if [[ $DEV_GITHUB_PASSWD ]]; then
    debug "Migrating Github password information to $HOME/.netrc"
    for mach in github.com api.github.com; do
        grep -q "^$mach" "$HOME/.netrc" &>/dev/null && continue
        printf "\nmachine %s login %s password %s\n" \
            "$mach" "$DEV_GITHUB_ID" "$DEV_GITHUB_PASSWD" >> "$HOME/.netrc"
    done
    chmod 600 "$HOME/.netrc"
    sed -ie 's/DEV_GITHUB_PASSWD=.*//' "$HOME/.build-crowbar.conf"
    debug "Please remove your embedded login information from the remote URLS."
fi

in_repo origin_remote &>/dev/null || \
    [[ $1 = setup ]] || \
    die "Unable to find origin account, please run setup!"


# Handle some global option parsing.
# This is mainly for --from and --to handling.
parse_opts_and_go() {
    local genargs=()
    local dev_cmd
    while (( $# > 0)); do
        case $1 in
            --from) shift
                in_repo git_remote_exists "$1" || \
                    die "You must pass a configured remote for --from."
                [[ ${DEV_FROM_REMOTES} ]] || DEV_FROM_REMOTES=()
                DEV_FROM_REMOTES+=("$1");;
            --to) shift
                in_repo git_remote_exists "$1" || \
                    die "You must pass a configured remote for --to."
                [[ ${DEV_TO_REMOTES} ]] || DEV_TO_REMOTES=()
                DEV_TO_REMOTES+=("$1");;
            --verbose) VERBOSE=true;;
            --quiet) VERBOSE=false;;
            --debug) DEBUG=true;;
            --dry-run) DRY_RUN=true;;
            --show-git-operations) SHOW_GIT_OPERATIONS=true;;
            *) genargs+=("$1");;
        esac
        shift
    done
    set -- "${genargs[@]}"
    dev_cmd="$1"
    shift
    if ! [[ $dev_cmd ]]; then
        dev_short_help
        exit 1
    fi
    [[ $(type -t "${DEV_COMMANDS[$dev_cmd]%% *}") = "function" ]] || \
        die "Command $dev_cmd is not implemented!"
    ${DEV_COMMANDS[$dev_cmd]} "$@"
}

parse_opts_and_go "$@"
